<script lang="ts" setup>
import { Plus, Close, ArrowLeftBold } from '@element-plus/icons-vue'
import { onMounted, ref, reactive, onUnmounted, nextTick, unref, computed } from 'vue'
import { calculateScaledDimensions } from '@/hooks/web/usePictureSize'
import { ImageCropping } from '@/components/ImageCroppings/index'
import { ElMessage, ElMessageBox, useZIndex, ElTooltip } from 'element-plus'
import { updatePoster } from '@/api/xr'
import Compressor from 'compressorjs'
import { useStorage } from '@/hooks/web/useStorage'
import {
getCavasList,
generateMaterials,
getHbImg,
addGenerateRecords,
generateInpaint,
uploadImages,
getPostersInfo,
getRecentPosters,
renamePosters,
getPosterInfo,
objectSegmentation,
matting,
generateExtend
} from '@/api/xr'
import * as fabric from 'fabric'
// import { fabric } from 'fabric'
// import { FabricImage } from 'fabric'
import { ArcText } from '@/extension/object/ArcText'
import { VerticalText } from '@/extension/object/VerticalText'
import generateRecord from '@/views/index/generateRecord.vue'
import leftTool from '@/views/index/leftTool.vue'
import hbTopTool from '@/views/index/hbTopTool.vue'
import proportionalValue from '@/views/index/proportionalValue.vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const { push } = useRouter()
import hbBottomTool from '@/views/index/hbBottomTool.vue'
import hbCreate from '@/views/index/hbCreate.vue'
import generateList from '@/views/index/generateList.vue'
import { throttle, debounce } from 'lodash'
const contrastImgRef2 = ref<InstanceType<typeof contrastImg2>>()
import contrastImg2 from '@/components/contrastImg/index2.vue'
import { v4 as uuidv4 } from 'uuid'
const canvasContainerRef = ref<HTMLElement>()
const parentDragResizRef = ref<HTMLElement>()
const canvasRef = ref<HTMLElement>()
const hbTopToolHtml = ref<any>(null)
const leftToolHtml = ref<any>(null)
const GenerateRecord = ref<any>(null)
const generateListHtml = ref<any>(null)
const hbCreateRef = ref<any>(null)
const enlargeSceneMatchRef = ref<any>(null)
const { getStorage, setStorage } = useStorage('localStorage')
import EnlargeSceneMatch from '@/views/index/EnlargeSceneMatch.vue' //一键扩景展示区
const state = reactive({
brushInfo: {} as any,
btnText: '素材生成',
contentPlaceholder: '描述你的素材创意。如:海边、日落、闪电、流星...',
gloLoading: false,
gloLoading2: false,
boxWidth: 500,
boxHeight: 500,
isChildActive: true,
toolbar: [
{
id: 1,
name: '自由模式',
aspectRatio: '1'
},
{
id: 2,
name: '原始比例',
aspectRatio: '1'
},
{
id: 3,
name: '1:1',
aspectRatio: '1/1'
},
{
id: 4,
name: '9:16',
aspectRatio: '9/16'
},
{
id: 5,
name: '16:9',
aspectRatio: '16/9'
},
{
id: 6,
name: '3:4',
aspectRatio: '3/4'
},
{
id: 7,
name: '4:3',
aspectRatio: '4/3'
}
],
toolbarActive: 0,
dialogdraggableResizable: false,
parentModeType: 1,
history: [] as any,
parentResultType: 0, //0:刚进入优化页面;1:打开消除笔;2:点击消除;3:点击修改图像
drawnPaths: [] as any, //笔画线条路径坐标点
imageData: '',
genarateHbArr: [] as any[], //记录文生图历史数据
brushComplete: false,
lastInpaintParams: {} as any, //记录上一次重绘的参数
generateName: '',
inpaintImageId: '',
inpaintImageUrl: '',
brushStatus: 0, //0:未选择;1:消除笔;2:修改图像
centerDialogVisible: false,
mattingDialogVisible: false,
mattingDialogVisible_Second: false,
imagePath:
'https://hips.hearstapps.com/hmg-prod/images/%E5%AE%8B%E6%99%BA%E5%AD%9D-1597774015.jpg?crop=0.500xw:1.00xh;0.500xw,0&resize=640:*',
chooseCanvasObject: {} as any,
_cw: 0, //画布宽高
_ch: 0,
away_top: 30, //去掉默认高度
baseMapUrl: '',
_w: 500, //初始比例/灰色层 宽高、定义裁剪区域
_h: 500,
zoom: 100, //缩放比例
isDragging: false,
isCtrlKey: false,
lastPosters: [] as any[],
imageWidth: 500,
imageHeight: 500,
iamgeInitUrl: '',
imageInitId: '',
initCtx: '',
points: [] as any[],
point: 1,
mattingObjectUrl: '',
mattingGenerate: {} as any,
baseMap: {
x: 100, //起始坐标
y: 50,
minWidth: 300,
minHeight: 400,
handles: ['tm', 'bm', 'ml', 'mr'],
// handles: ['tl', 'tm', 'tr', 'mr', 'br', 'bm', 'bl', 'ml'],
lockAspectRatio: false, //是否缩放宽高比
aspectRatio: 3 / 4, //图片宽高比例
changeW: 500, //扩图宽高
changeH: 500,
x2: 100, //子级边距
y2: 50
},
cropBg: false,
loading2: false,
contrastImgStatus: false,
kuotuText: '扩图',
kuotuTextloading: '一键扩景中',
generateData: {} as any,
interTime: null as any,
drdc: null as any,
imageObject: null as any,
loadingText: '',
oldInpimgW: 0,
oldInpimgH: 0,
guiding3: 1,
guiding4: 1
})
let canvas = {} as any
defineExpose({})
import { useEventBus } from '@/hooks/event/useEventBus'
import { format } from 'path'
import { tr } from 'element-plus/es/locale'
const { on, emit, off } = useEventBus()
// 计算 xx
const textareaNumber = computed(() => {
return 1
})
const setguiding3 = () => {
state.guiding3 = -1
setStorage('guiding3', -1)
}
const setguiding4 = () => {
state.guiding4 = -1
setStorage('guiding4', -1)
}
// 点击扩图
const enlargeMap = () => {
state.dialogdraggableResizable = true
nextTick(() => {
let data = {
imgUrl: state.baseMapUrl,
resolution: [state._w, state._h],
scale: '自由模式'
}
enlargeSceneMatchRef.value!.imgInfo(data)
})
}
// 操作区域居中
const getCenter = () => {
let boxW = parentDragResizRef.value!.offsetWidth
let boxH = parentDragResizRef.value!.offsetHeight
state.baseMap.x = (boxW - state.baseMap.changeW) / 2
state.baseMap.y = (boxH - state.baseMap.changeH) / 2
state.baseMap.x2 = Number(((state.baseMap.changeW - state._w * 0.8) / 2).toFixed())
state.baseMap.y2 = Number(((state.baseMap.changeH - state._h * 0.8) / 2).toFixed())
}
// 切换比例
const switchingRatio = (item, index) => {
state.toolbarActive = index
let data = {
imgUrl: state.baseMapUrl,
resolution: [state._w, state._h],
scale: item.name
}
enlargeSceneMatchRef.value!.imgInfo(data)
}
// 底图扩图 立即生成
const baseMapGeneration = () => {
console.log('底图扩图')
state.loading2 = true
let boxW = parentDragResizRef.value!.offsetWidth
let boxH = parentDragResizRef.value!.offsetHeight
contrastImgRef2.value?.getOldImg({ imgUrl: state.baseMapUrl }, boxW, boxH)
let params = enlargeSceneMatchRef.value!.genarateImage()
console.log(params)
params.imageId = getStorage('sourceImageId')
generateExtend(params)
.then((res) => {
if (res && res.code == 200) {
state.contrastImgStatus = true
state._w = res.data.width
state._h = res.data.height
state.kuotuText = '完成扩图'
let info = enlargeSceneMatchRef.value?.getImgInfo()
contrastImgRef2.value?.getNewImg({ imgUrl: res.data.imageUrl }, boxW, boxH, info)
state.generateData = res.data
return
}
})
.finally(() => {
state.loading2 = false
})
}
// 应用扩图
const playGenerate = () => {
state.dialogdraggableResizable = false
canvas.clipPath.left = state._cw / 2
canvas.clipPath.top = state._ch / 2 - state.away_top
canvas.clipPath.width = state._w
canvas.clipPath.height = state._h
canvas.getObjects().forEach(function (obj) {
// 检查对象的某些属性,例如 type, id 或其他自定义属性
if (obj.type === 'image' && obj.id === 'hbImg') {
console.log('找到了对象:', obj)
obj.setSrc(state.generateData.imageUrl, { crossOrigin: 'anonymous' }).then(() => {
canvas.renderAll()
})
}
})
//更新缓存数据
let hbMessage = getStorage('hbMessage')
state.baseMapUrl = state.generateData.imageUrl
hbMessage.imageUrl = state.generateData.imageUrl
hbMessage.imageId = state.generateData.imageId
hbMessage.backgroundImageSize.width = state.generateData.width
hbMessage.backgroundImageSize.height = state.generateData.height
setStorage('hbMessage', hbMessage)
// state.baseMap.minWidth = state._w = state.generateData.width
// state.baseMap.changeW = state._w //+ 200
// state.baseMap.minHeight = state._h = state.generateData.height
// state.baseMap.changeH = state._h //+ 100
// loadImageAndRect(result.data.backgroundImageUrl)
setStorage('sourceImageId', state.generateData.imageId)
setStorage('sourceImageUrl', state.generateData.imageUrl)
state.contrastImgStatus = false
state.kuotuText = '扩图'
let currentPosterId = getStorage('currentPosterId')
let params2 = {
posterId: currentPosterId,
type: 1,
imageList: [state.generateData.imageId]
}
addGenerateRecords(params2).then((res) => {
if (res.code === 200) {
GenerateRecord.value!.refGenerImg()
}
})
}
var startX = 0
var startY = 0 // 用于记录鼠标按下时的位置
const fonts = [
new FontFace('huiwenfangsong', 'url(fonts/huiwenfangsong.ttf)'),
new FontFace('FeiboZhengdianBody', 'url(fonts/FeiboZhengdianBody.otf)'),
new FontFace('SmileySans-Oblique', 'url(fonts/SmileySans-Oblique.ttf)'),
new FontFace('Fragrant-comfortable', 'url(fonts/Fragrant-comfortable.ttf)'),
new FontFace('Slidefu-Regular', 'url(fonts/Slidefu-Regular.ttf)'),
new FontFace('Optimal-Title', 'url(fonts/Optimal-Title.ttf)')
]
const queryData = async () => {
// 查询最近打开海报列表
getRecentPosters().then((res) => {
if (res && res.code == 200) {
state.lastPosters = res.data
}
})
}
// 缓存加载的图片
let cachedImage = null
// 预加载图片
const preloadImage = (src) => {
return new Promise((resolve, reject) => {
const img = new Image()
img.src = src
img.onload = () => resolve(img)
img.onerror = (err) => reject(err)
})
}
import imagePath from '@/assets/img/mtr.png'
let isDrawing = false
let drawingModePoints = [] as any
let currentPath
onMounted(async () => {
// 在初始化时加载图片
preloadImage(imagePath)
.then((img) => {
cachedImage = img // 缓存图片
})
.catch((err) => {
console.error('图片加载失败:', err)
})
state.guiding3 = getStorage('guiding3') != null ? getStorage('guiding3') : 1
state.guiding4 = getStorage('guiding4') != null ? getStorage('guiding4') : 1
document.getElementById('downImage').style.display = 'flex'
queryData()
let currentPosterId = getStorage('currentPosterId')
if (currentPosterId) {
let params = {
posterId: currentPosterId,
step: 1
}
updatePoster(params)
getPosterInfo({ posterId: currentPosterId }).then((result) => {
if (result && result.code == 200) {
state.generateName = result.data.name
setStorage('currentPosterName', result.data.name)
setStorage('hbMessage', result.data)
state.baseMap.minWidth = state._w = result.data.backgroundImageSize.width
state.baseMap.changeW = state._w //+ 200
state.baseMap.minHeight = state._h = result.data.backgroundImageSize.height
state.baseMap.changeH = state._h //+ 100
loadImageAndRect(result.data.backgroundImageUrl)
setStorage('sourceImageId', result.data.backgroundImageId)
setStorage('sourceImageUrl', result.data.backgroundImageUrl)
nextTick(() => {
generateListHtml.value!.updateGenerateName({ name: state.generateName })
requestAnimationFrame(() => {
Promise.all(fonts.map((font) => font.load())).then((loadedFonts) => {
// 将所有加载的字体添加到 `document.fonts`
loadedFonts.forEach((font) => document.fonts.add(font))
document.fonts.ready.then(() => {
initTextObject(JSON.parse(result.data.metaData) || [])
})
})
//开启定时保存
state.interTime = setInterval(() => {
saveData(getStorage('currentPosterId'))
}, 10000)
})
})
}
})
}
window.addEventListener('keydown', function (event) {
if (event.key == '' || event.keyCode == 32) {
}
if (event.ctrlKey) {
state.isCtrlKey = true
}
// console.log('空格键', state.isDragging)
console.log(event.key)
if (event.key == 'Delete') {
let obj = canvas.getActiveObjects()
if (obj) {
console.log('删除选中元素')
for (var i = 0; i < obj.length; i++) {
canvas.remove(obj[i])
}
canvas.discardActiveObject()
}
}
if (event.key == 'Backspace') {
let object = canvas.getActiveObjects()
if (object.length == 1) {
let activity = object[0]
if (textObject.includes(activity.type)) {
console.log(activity.isEditing)
if (activity.isEditing) {
return
}
}
}
for (var i = 0; i < object.length; i++) {
canvas.remove(object[i])
}
canvas.discardActiveObject()
}
})
window.addEventListener('keyup', function (event) {
if (event.key === ' ' || event.keyCode === 32) {
// 空格键的 keyCode 是 32
state.isDragging = false
// 停止拖动画布
canvas.defaultCursor = 'default' // 恢复鼠标指针样式为默认图标
}
if (event.ctrlKey) {
state.isCtrlKey = false
}
})
state._cw = canvasContainerRef.value!.offsetWidth
state._ch = canvasContainerRef.value!.offsetHeight
on('setZoom', setZoom)
initCanvas()
// initData()
window.addEventListener('resize', resizeCanvas)
nextTick(() => {})
on('saveDataByNew', (id) => {
console.log('保存咯')
saveData(id)
})
on('onDown', () => {
exportImage()
})
})
const generateSvgPath = (points) => {
if (points.length < 2) return ''
let path = M ${points[0].x} ${points[0].y}
for (let i = 1; i < points.length - 1; i++) {
const midX = (points[i].x + points[i + 1].x) / 2
const midY = (points[i].y + points[i + 1].y) / 2
path += ` Q ${points[i].x} ${points[i].y} ${midX} ${midY}`
}
return path
}
// 缩放画布
const setZoom = (scaleFactor) => {
state.zoom = scaleFactor
// console.log('设置缩放级别和中心点', scaleFactor, scaleFactor)
// 设置缩放级别和中心点
var zoom = scaleFactor / 100 // 计算新的缩放级别
var point = new fabric.Point(state._cw / 2, state._ch / 2 - state.away_top) // 中心点
// 应用缩放并调整画布位置以保持中心点不变
canvas.zoomToPoint(point, zoom)
}
onUnmounted(() => {
document.getElementById('downImage').style.display = 'none'
window.removeEventListener('resize', resizeCanvas)
clearInterval(state.interTime)
off('saveDataByNew')
off('onDown')
if (getStorage('currentPosterId') != null && getStorage('currentPosterId') != '') {
saveData(getStorage('currentPosterId'))
}
})
const initCanvas = async () => {
canvas.backgroundColor = 'rgba(0,0,0,0)' // 设置背景为透明
canvas.backgroundOpacity = 0 // 确保背景透明度为0
console.log('画布宽高', state._cw, state._ch)
canvas = new fabric.Canvas('canvasContainer', {
width: state._cw,
height: state._ch
// selectionFullyContained: true // 精准选择
}) // 实例化fabric,并绑定到canvas元素上
canvas.controlsAboveOverlay = true
;(fabric.Object.prototype.borderColor = '#008cff'), //'#008CFF'
// 设置所有新创建对象的角样式为 'circle'
(fabric.Object.prototype.cornerStyle = 'circle')
// 修改控制点的填充色为白色
fabric.Object.prototype.cornerColor = '#ffffff'
// 修改控制点的大小为10px
fabric.Object.prototype.cornerSize = 12
// 设置控制点不透明,即可以盖住其下的控制线
fabric.Object.prototype.transparentCorners = false
// 修改控制点的边框颜色为gray
灰色
fabric.Object.prototype.cornerStrokeColor = '#ffffff'
// 单独修改旋转控制点距离主体的纵向距离为-20px
// fabric.Object.prototype.controls.mtr.offsetY = -30
// // 单独修改旋转控制点,光标移动到该点上时的样式为pointer
,一个手的形状
// fabric.Object.prototype.controls.mtr.cursorStyle = 'pointer'
canvas.on('mouse:down', function (options) {
let e = options.e
if (e.altKey === true) {
// 开始拖动画布
canvas.defaultCursor = 'move' // 改变鼠标指针样式为移动图标
state.isDragging = true
startX = e.clientX // 记录鼠标按下时的 X 坐标
startY = e.clientY // 记录鼠标按下时的 Y 坐标
// console.log('鼠标按下状态?', startX, startY)
} else {
if (state.parentModeType == 2 && state.brushStatus == 2) {
//自由绘图模式
isDrawing = true
const pointer = canvas.getPointer(e)
drawingModePoints = [pointer] // 起点
} else {
var activeObjects = canvas.getActiveObjects()
if (activeObjects.length > 1) {
// 多个被选中时
let isAllText = true
activeObjects.forEach(function (obj) {
if (!textObject.includes(obj.type)) {
// 除了新选中的对象外,取消其他对象的选中状态
isAllText = false
}
})
// canvas.requestRenderAll(); // 重新渲染canvas以反映新的选择状态
if (isAllText) {
//代表选中的都是文字
//目前这里仅选中左侧文本,不做顶部属性变动
state.parentModeType = 4
}
} else if (activeObjects.length == 1) {
// 单个被选中时
let activeObject = activeObjects[0]
// console.log(activeObject.width)
if (textObject.includes(activeObject.type)) {
//此时单选选中的时文字的时候
console.log(activeObject.showIndex)
state.parentModeType = 4
leftToolHtml.value!.toolMode(4)
requestAnimationFrame(() => {
//父调子
hbTopToolHtml.value!.selectTextMessage(activeObject)
})
// if (activeObject.data != null && activeObject.data.effects != null) {
// // 当字体存在动画的时候
// setFontEffect(activeObject.data.effects)
// }
// if (activeObject.type == 'image') {
// state.parentModeType = 3
// leftToolHtml.value!.toolMode(3)
// }
} else if (activeObject.type == 'image') {
console.log(activeObject.showIndex)
state.parentModeType = 3
nextTick(() => {
requestAnimationFrame(() => {
console.log(hbTopToolHtml.value)
hbTopToolHtml.value!.useImg(activeObject)
leftToolHtml.value!.toolMode(3)
})
})
}
} else {
//未选中时
if (state.parentModeType == 3) {
hbTopToolHtml.value && hbTopToolHtml.value!.useStatus()
}
}
}
}
})
canvas.on('mouse:wheel', function (options) {
let e = options.e
e.preventDefault() // 阻止默认事件,防止页面滚动
e.stopPropagation() // 阻止事件冒泡
//&& !(state.parentModeType == 2 && (state.brushStatus == 1 || state.brushStatus == 2))
if (e.ctrlKey) {
state.isCtrlKey = true
var delta = e.deltaY || e.detail || e.wheelDelta
var zoom = canvas.getZoom()
var zoomIn = delta > 0 // 判断是缩放还是放大
zoom = zoomIn ? zoom - 0.1 : zoom + 0.1 // 根据需要调整缩放速度
zoom = Math.min(Math.max(0.1, zoom), 3) // 设置缩放范围限制
state.zoom = zoom * 100
}
})
canvas.on('mouse:move', function (options) {
let e = options.e
let opt = options.target
if (state.isDragging) {
// 如果正在拖动画布,则更新画布位置
let vpt = canvas.viewportTransform // 聚焦视图的转换
vpt[4] += e.clientX - startX
vpt[5] += e.clientY - startY
// canvas.setViewportTransform(vpt)
// canvas.relativePan({ x: -vpt[4], y: -vpt[5] })
canvas.requestRenderAll() // 重新渲染
startX = e.clientX
startY = e.clientY
}
//自由绘图模式
if (state.parentModeType == 2 && state.brushStatus == 2) {
canvas.selection = false
const offsetX = canvas.viewportTransform[4]
const offsetY = canvas.viewportTransform[5]
// console.log('画布偏移量', offsetX, offsetY)
const vpt = canvas.viewportTransform // 获取视口变换矩阵
const point = options.pointer
let x
let y
x = (point.x - vpt[4]) / vpt[3] // options.pointer.x
y = (point.y - vpt[5]) / vpt[3] //options.pointer.y
drawingCursor.set({ left: x, top: y, visible: true }).setCoords()
canvas.forEachObject((obj) => {
if (obj.isCursor) {
canvas.bringObjectToFront(obj)
// obj.bringToFront()
}
})
canvas.renderAll()
if (isDrawing) {
const pointer = canvas.getPointer(e)
drawingModePoints.push(pointer)
// 实时预览路径(可选,影响性能可去掉)
if (currentPath) canvas.remove(currentPath)
const pathStr = generateSvgPath(drawingModePoints)
console.log(pathStr)
currentPath = new fabric.Path(pathStr, {
stroke:
state.brushInfo.color || `rgba(255, 255, 255, ` + (state.brushInfo.opacity || 1) + `)`,
fill: '',
strokeWidth: state.brushInfo.width || 30,
strokeLineCap: 'round',
strokeLineJoin: 'round',
objectCaching: false,
selectable: false, //是否可以被选中
evented: false, //是否响应事件
hasBorders: false, //是否显示边框
hasControls: false // 不显示控制点
})
canvas.add(currentPath)
canvas.requestRenderAll()
}
}
//鼠标在不同区域显示
if (canvas.openDrawingMode) {
const vpt = canvas.viewportTransform // 获取视口变换矩阵
const point = options.pointer
// 将鼠标坐标转换到原始画布坐标系(考虑缩放和平移)
const rawX = (point.x - vpt[4]) / vpt[3] // // (当前X - 平移X)/缩放X:
const rawY = (point.y - vpt[5]) / vpt[3] // (当前Y - 平移Y)/缩放Y:
// 原始限制区域参数(假设state中的值为原始未缩放值)
const limitLeft = state._cw / 2 - state._w / 2
const limitTop = state._ch / 2 - state._h / 2 - state.away_top
const limitRight = state._cw / 2 + state._w / 2
const limitBottom = state._ch / 2 + state._h / 2 - state.away_top
// 判断是否在限制区域内
if (rawX < limitLeft || rawY < limitTop || rawX > limitRight || rawY > limitBottom) {
canvas.defaultCursor = 'default' // 区域外显示默认光标
} else {
canvas.defaultCursor = 'none' // 区域内隐藏光标
}
}
})
canvas.on('mouse:up', function (opt) {
if (isDrawing) {
// console.log('画笔坐标点集合:', drawingModePoints)
if (drawingModePoints.length == 1) {
//只点了一下没拖动
const circle = new fabric.Circle({
left: drawingModePoints[0].x,
top: drawingModePoints[0].y,
radius: state.brushInfo.width / 2 || 30, // 半径大小
fill:
state.brushInfo.color || `rgba(255, 255, 255, ` + (state.brushInfo.opacity || 1) + `)`,
originX: 'center',
originY: 'center',
selectable: false, //是否可以被选中
evented: false, //是否响应事件
hasBorders: false, //是否显示边框
hasControls: false // 不显示控制点
})
canvas.add(circle)
} else {
// 将临时 path 克隆为正式对象(可选中)
if (currentPath) {
/* currentPath.clone((clonedObj) => {
// clonedObj 是克隆后的对象
canvas.add(clonedObj)
}) */
// const finalPath = fabric.util.object.clone(currentPath)
// canvas.add(finalPath)
// canvas.remove(currentPath)
currentPath = null
}
}
state.drawnPaths.push(drawingModePoints)
state.history.push([...canvas.getObjects().filter((obj) => !obj['isCursor'])]) // 保存当前状态到历史记录中
drawingModePoints = []
}
canvas.selection = true
isDrawing = false
canvas.setViewportTransform(canvas.viewportTransform)
state.isDragging = false
state.isCtrlKey = false
})
canvas.on('mouse:out', function (opt) {
drawingCursor && drawingCursor.set({ visible: false }).setCoords()
canvas.renderAll()
})
canvas.on('object:scaling', function (options) {
let opt = options.target
const corner = options.transform.corner
if (corner === 'mtr') {
} else {
}
})
// state.history.push([...canvas.getObjects()])
canvas.on('path:created', (e) => {
const vpt = canvas.viewportTransform
// state.history.push([...canvas.getObjects().filter((obj) => !obj['isCursor'])]) // 保存当前状态到历史记录中
// const path = e.path as fabric.Path
// path.selectable = false
// state.drawnPaths.push(path)
})
}
// 退出画笔模式时清理光标
function disableDrawingMode() {
// 清除所有光标指示器
canvas.forEachObject((obj) => {
if (obj.isCursor) {
canvas.remove(obj)
}
})
canvas.openDrawingMode = false
canvas.defaultCursor = 'default'
canvas.hoverCursor = 'default'
canvas.renderAll()
}
const initData = () => {
let params = {
posterId: getStorage('currentPosterId')
}
getPostersInfo(params).then((res) => {
if (res.code === 200) {
let param = res.data
console.log(JSON.parse(param.metaData))
// const imgUrl = getStorage('sourceImageUrl')
loadImageAndRect(param.backgroundImageUrl)
requestAnimationFrame(() => {
document.fonts.ready.then(() => {
initTextObject(JSON.parse(param.metaData) || [])
})
})
}
})
}
/**
- 初始化加载文字、图片对象
*/
const initTextObject = (initData) => {
for (var i = 0; i < initData.length; i++) {
let params = initData[i]
if (textObject.includes(params.type)) {
if (params.type == 'VerticalText') {
let textbox = new VerticalText(params.text, {
left: params.left, // 文本框的 X 坐标
top: params.top, // 文本框的 Y 坐标
width: params.width, // 文本框的宽度
fontSize: params.fontSize, // 字体大小
borderColor: params.borderColor, // 边框颜色
fill: params.fill, // 文本颜色
hasControls: params.hasControls, // 允许调整大小和旋转
hasBorders: params.hasBorders, // 显示边框
textAlign: params.textAlign, // 文本对齐方式
editingBorderColor: params.editingBorderColor,
fontFamily: params.fontFamily,
splitByGrapheme: params.splitByGrapheme,
cornerStyle: params.cornerStyle, // 控制点样式
cornerColor: params.cornerColor, // 控制点颜色
transparentCorners: params.transparentCorners, // 控制点不透明
lockScalingFlip: params.lockScalingFlip, // 允许翻转
lockUniScaling: params.lockUniScaling, // 允许非等比缩放
selectable: true, // 确保对象可选择
evented: true, // 确保对象可交互
zIndex: params.zIndex,
data: params.data,
shadow: params.shadow,
stroke: params.stroke,
originX: params.originX,
originY: params.originY,
angle: params.angle,
showIndex: params.showIndex,
scaleX: params.scaleX,
scaleY: params.scaleY
})
textbox.setControlVisible('ml', false)
// textbox.setControlVisible('mt', false)
textbox.setControlVisible('mr', false)
// textbox.setControlVisible('mb', false)
textbox.selectionStart = textbox.text.length
textbox.selectionEnd = textbox.text.length
modifyControls(textbox)
canvas.add(textbox)
canvas.moveObjectTo(textbox, params.showIndex)
textbox.on('modified', function () {
// 计算新的字号
const newFontSize = textbox.fontSize * textbox.scaleX
console.log(newFontSize)
// 限制最小字号,防止过小导致不可见
if (newFontSize > 10) {
textbox.set({
fontSize: newFontSize,
scaleX: 1, // 归一化,防止 Fabric.js 的默认缩放影响
scaleY: 1
})
textbox.setCoords() // 更新坐标
canvas.renderAll()
hbTopToolHtml.value!.changeFontSize(newFontSize)
}
})
} else {
let textbox = new fabric.Textbox(params.text, {
left: params.left, // 文本框的 X 坐标
top: params.top, // 文本框的 Y 坐标
width: params.width, // 文本框的宽度
fontSize: params.fontSize, // 字体大小
borderColor: params.borderColor, // 边框颜色
fill: params.fill, // 文本颜色
hasControls: params.hasControls, // 允许调整大小和旋转
hasBorders: params.hasBorders, // 显示边框
textAlign: params.textAlign, // 文本对齐方式
editingBorderColor: params.editingBorderColor,
fontFamily: params.fontFamily,
splitByGrapheme: params.splitByGrapheme,
cornerStyle: params.cornerStyle, // 控制点样式
cornerColor: params.cornerColor, // 控制点颜色
transparentCorners: params.transparentCorners, // 控制点不透明
lockScalingFlip: params.lockScalingFlip, // 允许翻转
lockUniScaling: params.lockUniScaling, // 允许非等比缩放
selectable: params.selectable, // 确保对象可选择
evented: params.evented, // 确保对象可交互
zIndex: params.zIndex,
data: params.data,
shadow: params.shadow,
stroke: params.stroke,
originX: params.originX,
originY: params.originY,
angle: params.angle,
showIndex: params.showIndex,
scaleX: params.scaleX,
scaleY: params.scaleY
})
textbox.setControlVisible('ml', false)
textbox.setControlVisible('mt', false)
textbox.setControlVisible('mr', false)
textbox.setControlVisible('mb', false)
textbox.selectionStart = textbox.text.length
textbox.selectionEnd = textbox.text.length
modifyControls(textbox)
canvas.add(textbox)
// textbox.moveTo(params.showIndex)
canvas.moveObjectTo(textbox, params.showIndex)
textbox.on('modified', function () {
// 计算新的字号
const newFontSize = textbox.fontSize * textbox.scaleX
// 限制最小字号,防止过小导致不可见
if (newFontSize > 10) {
textbox.set({
fontSize: newFontSize,
scaleX: 1, // 归一化,防止 Fabric.js 的默认缩放影响
scaleY: 1,
width: textbox.width * textbox.scaleX
})
textbox.setCoords() // 更新坐标
canvas.renderAll()
hbTopToolHtml.value!.changeFontSize(newFontSize)
}
})
}
} else if (params.type == 'image') {
fabric.Image.fromURL(params.data.imageUrl, { crossOrigin: 'anonymous' }).then(
(img) => {
// 设置图片的位置和大小
img.set({
originX: params.originX,
originY: params.originY,
left: params.left, // 图片的左坐标
top: params.top, // 图片的顶坐标
width: params.width,
height: params.height,
selectable: params.selectable, // 禁用选择
evented: params.evented, // 禁用事件
data: params.data,
zIndex: params.zIndex,
angle: params.angle,
showIndex: params.showIndex,
scaleX: params.scaleX,
scaleY: params.scaleY,
cornerStyle: 'circle', // 控制点样式
cornerColor: 'white', // 控制点颜色
transparentCorners: false // 控制点不透明
})
// 添加图片到画布
modifyControls(img)
canvas.add(img)
// img.moveTo(params.showIndex)
canvas.moveObjectTo(img, params.showIndex)
},
{ crossOrigin: 'anonymous' }
)
}
}
// 重新渲染画布
canvas.renderAll()
}
/**
- 系统数据保存
*/
const saveData = (currentPosterId) => {
let allData = canvas.getObjects()
// console.log(allData)
let params: any[] = []
for (let i = 0; i < allData.length; i++) {
// console.log(allData[i])
if (textObject.includes(allData[i].type)) {
//文字类的
let textParams = {
text: allData[i].text,
fontSize: allData[i].fontSize,
type: allData[i].type,
fontWeight: allData[i].fontWeight,
fontFamily: allData[i].fontFamily,
textAlign: allData[i].textAlign,
fill: allData[i].fill,
left: allData[i].left, // 文本框的 X 坐标
top: allData[i].top, // 文本框的 Y 坐标
width: allData[i].width, // 文本框的宽度
borderColor: allData[i].borderColor, // 边框颜色
hasControls: allData[i].hasControls, // 允许调整大小和旋转
hasBorders: allData[i].hasBorders, // 显示边框
editingBorderColor: allData[i].editingBorderColor,
splitByGrapheme: allData[i].splitByGrapheme,
cornerStyle: allData[i].cornerStyle, // 控制点样式
cornerColor: allData[i].cornerColor, // 控制点颜色
transparentCorners: allData[i].transparentCorners, // 控制点不透明
lockScalingFlip: allData[i].lockScalingFlip, // 允许翻转
lockUniScaling: allData[i].lockUniScaling, // 允许非等比缩放
selectable: allData[i].selectable, // 确保对象可选择
evented: allData[i].evented, // 确保对象可交互
zIndex: allData[i].zIndex,
showIndex: allData[i].showIndex,
data: {
effect: allData[i].data.effect
},
shadow: allData[i].shadow,
stroke: allData[i].stroke,
originX: allData[i].originX,
originY: allData[i].originY,
angle: allData[i].angle,
scaleX: allData[i].scaleX,
scaleY: allData[i].scaleY
}
params.push(textParams)
} else if (allData[i].type == 'image' && allData[i].id != 'hbImg') {
//图片
console.log(allData[i].zIndex)
let imageParams = {
left: allData[i].left, // 图片的左坐标
top: allData[i].top, // 图片的顶坐标
width: allData[i].width,
height: allData[i].height,
originX: allData[i].originX,
originY: allData[i].originY,
selectable: allData[i].selectable, // 禁用选择
evented: allData[i].evented, // 禁用事件
data: {
imageId: allData[i].data.imageId,
imageUrl: allData[i].data.imageUrl,
effect: allData[i].data.effect
},
zIndex: allData[i].zIndex,
showIndex: allData[i].showIndex,
type: allData[i].type,
angle: allData[i].angle,
scaleX: allData[i].scaleX,
scaleY: allData[i].scaleY
}
params.push(imageParams)
}
}
// const currentPosterId = getStorage('currentPosterId')
let data = {
posterId: currentPosterId,
metaData: JSON.stringify(params)
}
updatePoster(data)
}
// 每隔 1 分钟执行一次保存
// setInterval(saveData, 60000);
/**
- 单个对象直接修改层级
*/
// const updatezIndex = () => {
// var activeObject = canvas.getActiveObject()
// activeObject.setZIndex(5)
// }
//淡入淡出
const fadeEffec = (activeObject) => {
let active = false
if (activeObject == null) {
active = true
activeObject = canvas.getActiveObject()
canvas.bringObjectToFront(activeObject)
// activeObject.bringToFront()
}
canvas.discardActiveObject()
if (textObject.includes(activeObject.type)) {
state.drdc = activeObject.animate(
{ opacity: 0 },
{
duration: 1000,
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {
state.drdc = activeObject.animate(
{ opacity: 1 },
{
duration: 1000,
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {
if (active) {
// activeObject.moveTo(activeObject.showIndex)
canvas.moveObjectTo(activeObject, activeObject.showIndex)
canvas.setActiveObject(activeObject)
}
canvas.renderAll()
state.gloLoading2 = false
}
}
)
}
}
)
}
}
/**
- 颜色渐变
*/
const colorFillFun = (activeObject) => {
let active = false
if (activeObject == null) {
active = true
activeObject = canvas.getActiveObject()
// activeObject.bringToFront()
canvas.bringObjectToFront(activeObject)
}
canvas.discardActiveObject()
if (textObject.includes(activeObject.type)) {
let color = activeObject.fill
let hue = 0
let animationFrame
function animateGradient() {
if (hue >= 360) {
cancelAnimationFrame(animationFrame) // 停止动画
activeObject.set('fill', color)
if (active) {
// activeObject.moveTo(activeObject.showIndex)
canvas.moveObjectTo(activeObject, activeObject.showIndex)
canvas.setActiveObject(activeObject)
}
canvas.renderAll()
state.gloLoading2 = false
return
}
hue += 5 // 每次增加色相
updateGradient(hue)
animationFrame = requestAnimationFrame(animateGradient)
}
animateGradient()
// 限制动画时长(如 3 秒后自动停止)
// setTimeout(() => {
// cancelAnimationFrame(animationFrame);
// activeObject.set('fill', color);
// canvas.renderAll();
// }, 3000);
function updateGradient(hue) {
const color1 = `hsl(${hue}, 100%, 50%)`
const color2 = `hsl(${(hue + 120) % 360}, 100%, 50%)`
activeObject.set(
'fill',
new fabric.Gradient({
type: 'linear',
coords: { x1: 0, y1: 0, x2: activeObject.width, y2: 0 },
colorStops: [
{ offset: 0, color: color1 },
{ offset: 1, color: color2 }
]
})
)
canvas.renderAll()
}
}
}
const rotateText = (activeObject) => {
let active = false
if (activeObject == null) {
active = true
activeObject = canvas.getActiveObject()
// activeObject.bringToFront()
canvas.bringObjectToFront(activeObject)
}
canvas.discardActiveObject()
if (textObject.includes(activeObject.type)) {
activeObject.animate(
{ angle: activeObject.angle + 360 },
{
onChange: canvas.renderAll.bind(canvas),
duration: 2000,
// easing: fabric.util.ease.easeInCubic,
onComplete: () => {
if (active) {
// activeObject.moveTo(activeObject.showIndex)
canvas.moveObjectTo(activeObject, activeObject.showIndex)
canvas.setActiveObject(activeObject)
}
canvas.renderAll()
state.gloLoading2 = false
}
}
)
}
}
const scaleX = (activeObject) => {
activeObject.animate(
{ scaleX: 1.2 },
{
duration: 500,
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {
activeObject.animate(
{ scaleX: 1 },
{
duration: 500,
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {}
}
)
}
}
)
}
const scaleY = (activeObject, active) => {
activeObject.animate(
{ scaleY: 1.2 },
{
duration: 500,
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {
activeObject.animate(
{ scaleY: 1 },
{
duration: 500,
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {
if (active) {
// activeObject.moveTo(activeObject.showIndex)
canvas.moveObjectTo(activeObject, activeObject.showIndex)
canvas.setActiveObject(activeObject)
}
// canvas.setActiveObject(activeObject)
canvas.renderAll()
}
}
)
}
}
)
}
//缩放
const scale = (activeObject) => {
let active = false
if (activeObject == null) {
active = true
activeObject = canvas.getActiveObject()
canvas.bringObjectToFront(activeObject)
// activeObject.bringToFront()
}
canvas.discardActiveObject()
if (textObject.includes(activeObject.type)) {
scaleX(activeObject)
scaleY(activeObject, active)
state.gloLoading2 = false
}
}
//左右移动
const movePosition = (activeObject) => {
let active = false
if (activeObject == null) {
active = true
activeObject = canvas.getActiveObject()
canvas.bringObjectToFront(activeObject)
// activeObject.bringToFront()
}
canvas.discardActiveObject()
if (textObject.includes(activeObject.type)) {
activeObject.animate(
{ left: activeObject.left - activeObject.width / 2 },
{
duration: 500,
easing: fabric.util.ease.easeInOutCubic, // 使用easing函数
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {
activeObject.animate(
{ left: activeObject.left + activeObject.width },
{
duration: 1000,
easing: fabric.util.ease.easeInOutCubic, // 使用easing函数
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {
activeObject.animate(
{ left: activeObject.left - activeObject.width / 2 },
{
duration: 500,
easing: fabric.util.ease.easeInOutCubic, // 使用easing函数
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {
if (active) {
// activeObject.moveTo(activeObject.showIndex)
canvas.moveObjectTo(activeObject, activeObject.showIndex)
canvas.setActiveObject(activeObject)
}
canvas.renderAll()
state.gloLoading2 = false
}
}
)
}
}
)
}
}
)
}
}
const setFontEffect = (effect) => {
console.log(canvas.getActiveObjects())
if (effect.effect == 0) {
state.gloLoading2 = false
}
if (canvas.getActiveObjects().length == 1) {
if (textObject.includes(canvas.getActiveObjects()[0].type)) {
let data = canvas.getActiveObjects()[0].data
if (data == null) {
canvas.getActiveObjects()[0].set('data', { effect: effect.effect })
} else {
data.effect = effect.effect
}
canvas.getActiveObjects()[0].set('data', data)
if (effect.effect == 1) {
state.gloLoading2 = true
fadeEffec(null)
}
if (effect.effect == 2) {
state.gloLoading2 = true
colorFillFun(null)
}
if (effect.effect == 3) {
state.gloLoading2 = true
rotateText(null)
}
if (effect.effect == 4) {
state.gloLoading2 = true
scale(null)
}
if (effect.effect == 5) {
state.gloLoading2 = true
movePosition(null)
}
}
}
}
// 图片抖动
const imgShake = (object) => {
let active = false
if (object == null) {
active = true
object = canvas.getActiveObject()
// object.bringToFront()
canvas.bringObjectToFront(object)
}
canvas.discardActiveObject()
if (object.type == 'image') {
shakeEffect(10)
}
// 抖动效果 offset-偏移量 count-执行次数 每次62.5ms 总共1秒
function shakeEffect(offset, count = 16) {
console.log('count:' + count)
var left
if (count % 4 == 0) {
left = object.left + offset
} else if (count % 4 == 1) {
left = object.left - offset
} else if (count % 4 == 2) {
left = object.left - offset
} else if (count % 4 == 3) {
left = object.left + offset
}
if (count == 0) {
if (active) {
// object.moveTo(object.showIndex)
canvas.moveObjectTo(object, object.showIndex)
canvas.setActiveObject(object)
}
// canvas.setActiveObject(object)
canvas.renderAll()
state.gloLoading2 = false
return
}
count--
object.animate(
{ left: left },
{
duration: 62.5,
onChange: canvas.renderAll.bind(canvas),
onComplete: () => shakeEffect(offset, count)
}
)
}
}
// 图片淡入淡出
const imgFadeEffec = (object) => {
let active = false
if (object == null) {
active = true
object = canvas.getActiveObject()
// object.bringToFront()
canvas.bringObjectToFront(object)
// state.drdc.cancel();
// fabric.util.animate.stop(object);
}
canvas.discardActiveObject()
if (object.type == 'image') {
object.animate(
{ opacity: 0 },
{
duration: 1000,
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {
object.animate(
{ opacity: 1 },
{
duration: 1000,
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {
if (active) {
// object.moveTo(object.showIndex)
canvas.moveObjectTo(object, object.showIndex)
canvas.setActiveObject(object)
}
canvas.renderAll()
state.gloLoading2 = false
}
}
)
}
}
)
}
}
//图片旋转
const imgRotate = (object) => {
let active = false
if (object == null) {
active = true
object = canvas.getActiveObject()
// object.bringToFront()
canvas.bringObjectToFront(object)
}
canvas.discardActiveObject()
if (object.type == 'image') {
object.animate(
{ angle: object.angle + 360 },
{
duration: 2000,
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {
if (active) {
// object.moveTo(object.showIndex)
canvas.moveObjectTo(object, object.showIndex)
canvas.setActiveObject(object)
}
canvas.renderAll()
state.gloLoading2 = false
}
// onComplete: () => {
// object.animate({'angle': object.angle + 360}, {
// duration: 1000,
// onChange: canvas.renderAll.bind(canvas)
// })
// }
}
)
}
}
//图片缩放
const imgScale = (object) => {
let active = false
if (object == null) {
active = true
object = canvas.getActiveObject()
// object.bringToFront()
canvas.bringObjectToFront(object)
}
const start = object.scaleX
const end = object.scaleX * 1.2
canvas.discardActiveObject()
if (object.type == 'image') {
fabric.util.animate({
startValue: start,
endValue: end,
duration: 1000,
onChange: function (value) {
object.set({ scaleX: value, scaleY: value })
canvas.renderAll()
},
onComplete: () => {
fabric.util.animate({
startValue: end,
endValue: start,
duration: 1000,
onChange: function (value) {
object.set({ scaleX: value, scaleY: value })
canvas.renderAll()
},
onComplete: () => {
if (active) {
// object.moveTo(object.showIndex)
canvas.moveObjectTo(object, object.showIndex)
canvas.setActiveObject(object)
}
canvas.renderAll()
state.gloLoading2 = false
}
})
}
})
}
}
// 图片翻转
function imgFlip(object) {
let active = false
if (object == null) {
active = true
object = canvas.getActiveObject()
// object.bringToFront()
canvas.bringObjectToFront(object)
}
canvas.discardActiveObject()
if (object.type == 'image') {
object.animate(
{ scaleX: -1 * object.scaleX },
{
duration: 1000,
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {
object.animate(
{ scaleX: -1 * object.scaleX },
{
duration: 1000,
onChange: canvas.renderAll.bind(canvas),
onComplete: () => {
if (active) {
// object.moveTo(object.showIndex)
canvas.moveObjectTo(object, object.showIndex)
canvas.setActiveObject(object)
}
canvas.renderAll()
state.gloLoading2 = false
}
}
)
// canvas.setActiveObject(object)
canvas.renderAll()
}
}
)
}
}
const imgDynamicEffect = (effect) => {
// if (canvas.getActiveObjects().length == 1 && canvas.getActiveObjects()[0].type == 'image') {
// state.imageObject = canvas.getActiveObjects()[0];
// // fabric.runningAnimations.cancelByTarget(state.imageObject);
// } else {
// if (state.imageObject == null) {
// return;
// } else {
// if (fabric.runningAnimations) {
// fabric.runningAnimations.cancelByTarget(state.imageObject);
// }
// let data = state.imageObject.data;
// if (data.effect == 1) {
// state.imageObject.set("opacity",1);
// }
// if (data.effect == 2) {
// state.imageObject.set("angle",0);
// }
// canvas.renderAll();
// canvas.setActiveObject(state.imageObject);
// }
// }
// if (state.imageObject == null) {
// console.log("1122");
// if (canvas.getActiveObjects().length == 1 && canvas.getActiveObjects()[0].type == 'image') {
// state.imageObject = canvas.getActiveObjects()[0];
// } else {
// return;
// }
// } else {
// if (canvas.getActiveObjects().length == 1 && canvas.getActiveObjects()[0].type == 'image') {
// if (canvas.getActiveObjects()[0].id == state.imageObject.id) {
// if (fabric.runningAnimations) {
// fabric.runningAnimations.cancelByTarget(state.imageObject);
// }
// let data = state.imageObject.data;
// if (data.effect == 1) {
// state.imageObject.set("opacity", 1);
// }
// if (data.effect == 2) {
// state.imageObject.set("angle", 0);
// }
// canvas.renderAll();
// canvas.setActiveObject(state.imageObject);
// } else {
// state.imageObject = canvas.getActiveObjects()[0];
// }
// }
// }
if (canvas.getActiveObjects().length == 1) {
if (canvas.getActiveObjects()[0].type == 'image') {
let data = canvas.getActiveObjects()[0].data
if (data == null) {
canvas.getActiveObjects()[0].set('data', { effect: effect.effect })
} else {
data.effect = effect.effect
}
canvas.getActiveObjects()[0].set('data', data)
if (effect.effect == 0) {
state.gloLoading2 = false
}
if (effect.effect == 1) {
//淡入淡出
state.gloLoading2 = true
imgFadeEffec(null)
}
if (effect.effect == 2) {
//旋转
state.gloLoading2 = true
imgRotate(null)
}
if (effect.effect == 3) {
//缩放
state.gloLoading2 = true
imgScale(null)
}
if (effect.effect == 4) {
//翻转
state.gloLoading2 = true
imgFlip(null)
}
if (effect.effect == 5) {
//抖动
state.gloLoading2 = true
imgShake(null)
}
}
}
}
let textObject = ['VerticalText', 'textbox', 'text']
/**
- 修改文字颜色
*/
const updateTextColor = async (color) => {
var activeObject = canvas.getActiveObjects()
console.log(color)
for (let i = 0; i < activeObject.length; i++) {
console.log('选中的对象:', activeObject[i].type)
if (textObject.includes(activeObject[i].type)) {
activeObject[i].set('fill', color.color)
canvas.renderAll()
}
}
}
/**
- 修改文字字体大小
*/
const updateTextFontSize = (fontSize) => {
var activeObject = canvas.getActiveObjects()
for (let i = 0; i < activeObject.length; i++) {
if (textObject.includes(activeObject[i].type)) {
activeObject[i].set('fontSize', fontSize.fontSize)
canvas.renderAll()
}
}
}
/**
- 修改字体粗细
*/
const updateTextFontBold = (type) => {
var activeObject = canvas.getActiveObjects()
for (let i = 0; i < activeObject.length; i++) {
if (textObject.includes(activeObject[i].type)) {
activeObject[i].set('fontWeight', type.fontBold)
canvas.renderAll()
}
}
//normal bold
}
/**
- 修改字体
*/
const updateTextFontFamily = (fontfamily) => {
var activeObject = canvas.getActiveObjects()
for (let i = 0; i < activeObject.length; i++) {
if (textObject.includes(activeObject[i].type)) {
activeObject[i].set('fontFamily', fontfamily)
}
}
}
/**
- 修改文字位置
*/
const updateTextFontPosition = (position) => {
var activeObject = canvas.getActiveObjects()
for (let i = 0; i < activeObject.length; i++) {
if (textObject.includes(activeObject[i].type) && activeObject[i].type != 'VerticalText') {
activeObject[i].set('textAlign', position.type)
canvas.renderAll()
}
}
}
/**
- 修改文字字体
*/
const updateFontFamily = (type) => {
var activeObject = canvas.getActiveObjects()
for (let i = 0; i < activeObject.length; i++) {
if (textObject.includes(activeObject[i].type)) {
activeObject[i].set('fontFamily', type.fontFamily)
canvas.renderAll()
}
}
}
const captureImg = (activity) => {
//导出静态图
// const canvas = document.getElementById('your-canvas');
// const dataURL = canvas.toDataURL('image/png')
state.zoom = 100 //缩放重置
// var transform = canvas.viewportTransform.slice()
canvas.viewportTransform = [1, 0, 0, 1, 0, 0]
var dataURL = canvas.toDataURL({
format: 'png', // 可以是 'jpeg', 'png', 等
quality: 1, // 仅当格式为 'jpeg' 时有效
left: state._cw / 2 - state._w / 2, // 裁剪区域的左上角 x 坐标
top: state._ch / 2 - state.away_top - state._h / 2, // 裁剪区域的左上角 y 坐标
width: state._w, // 裁剪区域的宽度
height: state._h // 裁剪区域的高度
})
// console.log('导出指定区域', dataURL)
//下载图片
const link = document.createElement('a')
link.download = uuidv4() + '.png'
link.href = dataURL
link.click()
state.gloLoading = false
link.remove()
if (activity.length > 0) {
canvas.setActiveObject(activity[0])
}
// canvas.viewportTransform = transform
}
/**
- 截取gif图片
*/
const capturerFun = (activity) => {
state.zoom = 100 //缩放重置
// var transform = canvas.viewportTransform.slice()
canvas.viewportTransform = [1, 0, 0, 1, 0, 0]
const capturer = new CCapture({ format: 'gif', framerate: 30, workersPath: './', name: uuidv4() })
// 👇 指定导出区域
const clipX = state._cw / 2 - state._w / 2
const clipY = state._ch / 2 - state.away_top - state._h / 2
const clipWidth = state._w
const clipHeight = state._h
// 创建离屏 canvas 用于裁剪导出区域
const offscreenCanvas = document.createElement('canvas')
offscreenCanvas.width = clipWidth
offscreenCanvas.height = clipHeight
const offscreenCtx = offscreenCanvas.getContext('2d')
const startRecording = () => {
capturer.start()
}
const stopRecording = () => {
capturer.stop()
// canvas.viewportTransform = transform
capturer.save((blob) => {
// ✅ 1. 你可以在这里拿到完整的 GIF Blob 文件
// ✅ 2. 创建下载链接
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = uuidv4() + '.gif'
document.body.appendChild(a)
// ✅ 3. 模拟下载(监听 Blob 被完整加载)
fetch(url)
.then((res) => res.blob())
.then(() => {
// ✅ 下载内容已经读取完成(或至少 ready)
state.gloLoading = false
URL.revokeObjectURL(url) // 清理
a.remove()
})
// 触发下载
a.click()
})
}
let angle = 0
const captureClippedFrame = () => {
// 复制主 canvas 指定区域到离屏 canvas
offscreenCtx.clearRect(0, 0, clipWidth, clipHeight)
offscreenCtx.drawImage(
canvas.lowerCanvasEl, // 原始 fabric canvas
clipX,
clipY,
clipWidth,
clipHeight, // 要截取的区域
0,
0,
clipWidth,
clipHeight // 画到离屏 canvas 的位置
)
capturer.capture(offscreenCanvas) // 捕捉离屏 canvas(即裁剪区域)
}
const animate = () => {
captureClippedFrame() // 录制当前帧(只导出部分区域)
if (angle < 60) {
angle++
requestAnimationFrame(animate)
} else {
stopRecording() // 录制完成
angle = 0
if (activity.length > 0) {
canvas.setActiveObject(activity[0])
}
}
}
startRecording()
animate()
}
/**
- 修改文字横版竖版
*/
const setDirection = () => {
// const placeholderText = '请输入文字...'
// let textbox = new VerticalText(placeholderText, {
// left: state._cw / 2, // 文本框的 X 坐标
// top: state._ch / 2 - state.away_top, // 文本框的 Y 坐标
// // width: 300, // 文本框的宽度
// fontSize: 42, // 字体大小
// borderColor: 'white', // 边框颜色
// fill: 'white', // 文本颜色
// hasControls: true, // 允许调整大小和旋转
// hasBorders: true, // 显示边框
// // textAlign: 'left', // 文本对齐方式
// // editingBorderColor: 'red',
// fontFamily: 'huiwenfangsong',
// splitByGrapheme: true,
// cornerStyle: 'circle', // 控制点样式
// cornerColor: 'white', // 控制点颜色
// transparentCorners: false, // 控制点不透明
// lockScalingFlip: false, // 允许翻转
// lockUniScaling: false, // 允许非等比缩放
// selectable: true, // 确保对象可选择
// evented: true, // 确保对象可交互
// data: {
// effect: 0
// },
// originX: 'center',
// originY: 'center',
// })
// canvas.add(textbox)
// canvas.setActiveObject(textbox)
// return;
let objectActivity = canvas.getActiveObjects()
if (objectActivity.length != 1) {
return
}
let obj = objectActivity[0]
canvas.remove(obj)
if (obj.type == 'VerticalText') {
//当前竖版,改成横版
let arctext = new fabric.Textbox(obj.text, {
left: obj.left, // 文本框的 X 坐标
top: obj.top, // 文本框的 Y 坐标
width: obj.height, // 文本框的宽度
fontSize: obj.fontSize, // 字体大小
borderColor: obj.borderColor, // 边框颜色
fill: obj.fill, // 文本颜色
hasControls: true, // 允许调整大小和旋转
hasBorders: true, // 显示边框
textAlign: 'left', // 文本对齐方式
// editingBorderColor: 'red',
fontFamily: obj.fontFamily,
splitByGrapheme: true,
cornerStyle: 'circle', // 控制点样式
cornerColor: 'white', // 控制点颜色
transparentCorners: false, // 控制点不透明
lockScalingFlip: false, // 允许翻转
lockUniScaling: false, // 允许非等比缩放
selectable: true, // 确保对象可选择
evented: true, // 确保对象可交互
data: {
effect: 0
},
originX: 'center',
originY: 'center',
showIndex: obj.showIndex,
shadow: obj.shadow,
stroke: obj.stroke
})
canvas.add(arctext)
canvas.setActiveObject(arctext)
arctext.enterEditing()
arctext.on('modified', function () {
// 计算新的字号
const newFontSize = arctext.fontSize * arctext.scaleX
console.log(newFontSize)
// 限制最小字号,防止过小导致不可见
if (newFontSize > 10) {
arctext.set({
fontSize: newFontSize,
scaleX: 1, // 归一化,防止 Fabric.js 的默认缩放影响
scaleY: 1,
width: arctext.width * arctext.scaleX
})
arctext.setCoords() // 更新坐标
canvas.renderAll()
hbTopToolHtml.value!.changeFontSize(newFontSize)
}
})
} else {
let arctext = new VerticalText(obj.text, obj)
canvas.add(arctext)
canvas.setActiveObject(arctext)
arctext.enterEditing()
arctext.on('modified', function () {
// 计算新的字号
const newFontSize = arctext.fontSize * arctext.scaleX
console.log(newFontSize)
// 限制最小字号,防止过小导致不可见
if (newFontSize > 10) {
arctext.set({
fontSize: newFontSize,
scaleX: 1, // 归一化,防止 Fabric.js 的默认缩放影响
scaleY: 1
})
arctext.setCoords() // 更新坐标
canvas.renderAll()
hbTopToolHtml.value!.changeFontSize(newFontSize)
}
})
}
canvas.renderAll()
}
/**
- 添加文字类操作
- @param type 类型 目前添加的时候暂时用不到type
*/
const addText = (type, options) => {
let length = canvas.getObjects().length
console.log(length)
const placeholderText = '请输入文字...'
let textbox = new fabric.Textbox(placeholderText, {
left: state._cw / 2, // 文本框的 X 坐标
top: state._ch / 2 - state.away_top, // 文本框的 Y 坐标
width: 300, // 文本框的宽度
fontSize: 42, // 字体大小
borderColor: 'white', // 边框颜色
fill: 'white', // 文本颜色
backgroundColor: '',
hasControls: true, // 允许调整大小和旋转
hasBorders: true, // 显示边框
textAlign: 'left', // 文本对齐方式
// editingBorderColor: 'red',
fontFamily: 'huiwenfangsong',
splitByGrapheme: true,
cornerStyle: 'circle', // 控制点样式
cornerColor: 'white', // 控制点颜色
transparentCorners: false, // 控制点不透明
lockScalingFlip: false, // 允许翻转
lockUniScaling: false, // 允许非等比缩放
selectable: true, // 确保对象可选择
evented: true, // 确保对象可交互
data: {
effect: 0
},
originX: 'center',
originY: 'center',
showIndex: length
})
// textbox.setControlVisible('ml', false)
textbox.setControlVisible('mt', false)
// textbox.setControlVisible('mr', false)
textbox.setControlVisible('mb', false)
textbox.selectionStart = textbox.text.length
textbox.selectionEnd = textbox.text.length
modifyControls(textbox)
canvas.add(textbox)
canvas.setActiveObject(textbox)
textbox.enterEditing()
textbox.on('modified', function () {
// 计算新的字号
const newFontSize = textbox.fontSize * textbox.scaleX
console.log(newFontSize)
// 限制最小字号,防止过小导致不可见
if (newFontSize > 10) {
textbox.set({
fontSize: newFontSize,
scaleX: 1, // 归一化,防止 Fabric.js 的默认缩放影响
scaleY: 1,
width: textbox.width * textbox.scaleX
})
textbox.setCoords() // 更新坐标
canvas.renderAll()
hbTopToolHtml.value!.changeFontSize(newFontSize)
}
})
// 重新渲染画布
canvas.renderAll()
hbTopToolHtml.value!.selectTextMessage(textbox)
}
let drawingCursor
// 点击修改图像 进入自由绘制模式
const drawFree = (param) => {
if (canvas) {
if (param.status) {
// 设置缩放级别和中心点
/* var zoom = 1 // 计算新的缩放级别
var point = new fabric.Point(state._cw / 2, state._ch / 2 - state.away_top) // 中心点
// 应用缩放并调整画布位置以保持中心点不变
canvas.zoomToPoint(point, zoom) */
state.parentResultType = param.status
if (param.status === 1) {
//点击消除笔
state.brushStatus = 1
} else {
//点击修改图像
state.brushStatus = 2
}
// 创建一个新的圆形指示器
drawingCursor = new fabric.Circle({
originX: 'center',
originY: 'center',
left: state._cw / 2,
top: state._ch / 2,
radius: param.width / 2,
fill: param.color || `rgba(255, 255, 255, ` + (param.opacity || 1) + `)`, //圆形颜色
selectable: false,
evented: false, // 不影响事件
isCursor: true, // 自定义标识,方便移除
visible: false
})
state.brushInfo = param
//取消全部选中
canvas.discardActiveObject()
requestAnimationFrame(() => {
//将底图放在最上层
canvas.getObjects().forEach((obj) => {
if (obj.id == 'hbImg') {
// obj.bringToFront()
canvas.bringObjectToFront(obj)
}
if (obj.id != 'hbImg') {
obj.visible = false
}
})
state.history = []
state.drawnPaths = []
//鼠标光标设置为圆形
canvas.bringObjectToFront(drawingCursor)
canvas.add(drawingCursor)
canvas.openDrawingMode = true
// 设置鼠标光标为 none,隐藏默认十字形
/* canvas.freeDrawingCursor = 'none'
var freeDrawingBrush = new fabric.PencilBrush(canvas)
canvas.freeDrawingBrush = freeDrawingBrush
canvas.freeDrawingBrush.color =
param.color || `rgba(255, 255, 255, ` + (param.opacity || 1) + `)` // 设置画笔颜色
canvas.freeDrawingBrush.width = param.width || 30
canvas.renderAll()
updateBrushOpacity({ bushOpacity: param.opacity || 1, color: param.color || '#ffffff' }) */
state.history.push([...canvas.getObjects()])
})
} else {
//取消消除笔
canvas.openDrawingMode = false
state.parentResultType = 0
brushClear()
disableDrawingMode()
//将底图放在最底层
canvas.getObjects().forEach((obj) => {
if (obj.id != 'hbImg') {
obj.visible = true
}
if (obj.id == 'hbImg') {
// obj.moveTo(1)
canvas.moveObjectTo(obj, 0)
}
})
setZoom(state.zoom)
}
}
}
//画笔撤销
const brushUndo = () => {
console.log('画笔撤销')
if (state.history.length > 1) {
// 至少有一个状态可以撤销(除了初始状态)
state.history.pop() // 获取上一个状态
// console.log('删除画布上的元素包括底图', state.history.length)
// canvas.loadFromJSON(previousState, canvas.renderAll.bind(canvas)) // 加载上一个状态并渲染
if (state.drawnPaths.length > 0) {
const lastPath = state.drawnPaths.pop()
// console.log('删除记录的点', state.drawnPaths.length)
canvas?.remove(lastPath as fabric.Path)
}
canvas?.clear()
const lastState = state.history[state.history.length - 1]
if (lastState) {
lastState.forEach((object) => {
canvas?.add(object)
})
}
} else {
console.log('No more history to undo.') // 没有更多历史记录可以撤销时的提示
}
}
//点击消除按钮
const clearCommit = (param) => {
if (canvas) {
state.zoom = 100 //缩放重置
canvas.viewportTransform = [1, 0, 0, 1, 0, 0]
requestAnimationFrame(() => {
// 隐藏除画笔外的元素
canvas.getObjects().forEach((obj) => {
if (obj.type != 'path') {
obj.set({
visible: false
})
}
})
state.imageData = canvas.toDataURL({
format: 'png', // 可以是 'jpeg', 'png', 等
quality: 1, // 仅当格式为 'jpeg' 时有效
left: state._cw / 2 - state._w / 2, // 裁剪区域的左上角 x 坐标
top: state._ch / 2 - state.away_top - state._h / 2, // 裁剪区域的左上角 y 坐标
width: state._w, // 裁剪区域的宽度
height: state._h // 裁剪区域的高度
})
canvas.getObjects().forEach((obj) => {
if (obj.type != 'path') {
obj.set({
visible: true
})
}
})
console.log(state.imageData)
if (state.imageData) {
// console.log('state.imageData', state.imageData)
// 将 Base64 数据转换为 Blob 对象
const byteCharacters = atob(state.imageData.split(',')[1])
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
const blob = new Blob([byteArray], { type: 'image/png' })
// 创建 FormData 对象
const formData = new FormData()
formData.append('file', blob, 'drawing.png')
const url = window.location.origin + '/api/v2/upload/images'
fetch(url, {
method: 'POST', // 或者使用 'post'(不区分大小写)
body: formData
})
.then((response) => {
if (!response.ok) {
ElMessage.error('网络响应不是ok状态')
}
return response.json() // 如果需要,可以返回解析后的JSON数据
})
.then((res) => {
const result = res.data
const sourceImageId = getStorage('sourceImageId')
const currentPosterId = getStorage('currentPosterId')
if (result) {
state.parentResultType = 2
state.brushComplete = true
canvas.openDrawingMode = false
let params = {
maskImageId: result.imageId,
sourceImageId: sourceImageId,
mode: param.status,
prompt: param.status == 1 ? param.content : ''
}
state.lastInpaintParams = params
state.gloLoading = true
state.loadingText = '图片重绘中...'
//海报图片重绘
generateInpaint(params)
.then((res) => {
if (res.code === 200 && res.data && res.data.imageId) {
state.inpaintImageId = res.data.imageId
state.inpaintImageUrl = res.data.imageUrl
let params2 = {
posterId: currentPosterId,
type: 1,
imageList: [res.data.imageId]
}
saveVocabulary(res.data)
addGenerateRecords(params2)
canvas?.clear()
loadImageAndRect(res.data.imageUrl)
}
})
.finally(() => {
state.gloLoading = false
})
}
})
.catch(() => {
ElMessage.error('上传失败')
})
}
})
}
}
const reClear = () => {
let currentPosterId = getStorage('currentPosterId')
state.gloLoading = true
state.loadingText = '图片重绘中...'
generateInpaint(state.lastInpaintParams)
.then((res) => {
if (res.code === 200) {
state.inpaintImageId = res.data.imageId
state.inpaintImageUrl = res.data.imageUrl
let params2 = {
posterId: currentPosterId,
type: 1,
imageList: [res.data.imageId]
}
saveVocabulary(res.data)
addGenerateRecords(params2)
canvas?.clear()
loadImageAndRect(res.data.imageUrl)
}
})
.finally(() => {
state.gloLoading = false
})
}
//重新编辑
const reEdit = (req) => {
// canvas.clear()
//加载原图
brushClear()
state.brushComplete = false
hbTopToolHtml.value!.openEraser(req)
}
//完成
const complete = async () => {
state.history = []
state.drawnPaths = []
state.history.push([...canvas.getObjects()])
setStorage('sourceImageId', state.inpaintImageId)
setStorage('sourceImageUrl', state.inpaintImageUrl)
canvas?.clear()
loadImageAndRect(state.inpaintImageUrl)
state.parentResultType = 0
state.brushComplete = false
canvas.openDrawingMode = false
hbTopToolHtml.value!.closeBrush()
let currentPosterId = getStorage('currentPosterId')
getPosterInfo({ posterId: currentPosterId }).then((result) => {
if (result && result.code == 200) {
requestAnimationFrame(() => {
Promise.all(fonts.map((font) => font.load())).then((loadedFonts) => {
// 将所有加载的字体添加到 `document.fonts`
loadedFonts.forEach((font) => document.fonts.add(font))
document.fonts.ready.then(() => {
initTextObject(JSON.parse(result.data.metaData) || [])
})
})
})
}
})
let params = {
posterId: currentPosterId,
backgroundImageId: state.inpaintImageId,
backgroundImageUrl: state.inpaintImageUrl
}
updatePoster(params)
GenerateRecord.value!.refGenerImg()
setZoom(state.zoom)
}
// 保持历史记录(localStorage)
const saveVocabulary = (historicalImgInfo: any) => {
if (JSON.parse(getStorage('genarateHbHistory')) === null) {
state.genarateHbArr = [historicalImgInfo]
setStorage('genarateHbHistory', JSON.stringify(state.genarateHbArr))
return
}
// 获取历史记录
let genarateHbHistoryArr = JSON.parse(getStorage('genarateHbHistory'))
// 添加新条目
genarateHbHistoryArr = [historicalImgInfo, ...genarateHbHistoryArr].slice(0, 20) // 确保历史记录不超过20条
// 更新本地存储
setStorage('genarateHbHistory', JSON.stringify(genarateHbHistoryArr))
// 更新组件状态
state.genarateHbArr = genarateHbHistoryArr
}
// 清空画布
const brushClear = async () => {
const firstState = state.history[0]
if (firstState) {
canvas?.clear()
firstState.forEach((object) => {
canvas?.add(object)
})
//将底图放在最上层
canvas.getObjects().forEach((obj) => {
if (obj.id == 'hbImg') {
// obj.bringToFront()
canvas.bringObjectToFront(obj)
}
})
canvas.forEachObject((obj) => {
if (obj.isCursor) {
// obj.bringToFront()
canvas.bringObjectToFront(obj)
}
})
}
await nextTick()
state.history = []
state.drawnPaths = []
state.history.push([...canvas.getObjects()])
}
// 画笔颜色
const updateBrushColor = (param) => {
if (canvas) {
const color = hexToRgba(param.color, param.bushOpacity)
const rgb = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d\.]+))?\)$/)
if (rgb && rgb.length === 5) {
state.brushInfo.color = `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${param.bushOpacity})`
}
drawingCursor.set({ fill: param.color || `rgba(255, 255, 255, ` + (param.opacity || 1) + `)` })
}
}
const updateBrushSize = (param) => {
if (canvas) {
state.brushInfo.width = param.brushSize
drawingCursor.set({ radius: param.brushSize / 2 })
}
}
const updateBrushOpacity = (param) => {
if (canvas) {
const color = hexToRgba(param.color, param.bushOpacity)
const rgb = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d\.]+))?\)$/)
if (rgb && rgb.length === 5) {
state.brushInfo.color = `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${param.bushOpacity})`
// canvas.freeDrawingBrush.color = `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${param.bushOpacity})`
}
}
}
//16进制颜色值转rgb值
const hexToRgba = (hex, alpha = 1) => {
// 检查十六进制颜色值是否有效
if (!/^#[0-9A-F]{6}$/i.test(hex)) {
throw new Error('Invalid HEX color.')
}
// 提取红色、绿色和蓝色分量
const bigint = parseInt(hex.slice(1), 16)
const r = (bigint >> 16) & 255
const g = (bigint >> 8) & 255
const b = bigint & 255
// 返回RGBA格式的颜色字符串
return rgba(${r}, ${g}, ${b}, ${alpha})
}
const resizeCanvas = () => {
state._cw = canvasContainerRef.value!.offsetWidth
state._ch = canvasContainerRef.value!.offsetHeight
console.log('画布宽高', state._cw, state._ch)
if (canvas) {
canvas.setWidth(state._cw)
canvas.setHeight(state._ch)
canvas.renderAll()
}
}
// 左侧工具栏 控制 顶部工具栏 显示
const toolModes = (modeType) => {
state.parentModeType = modeType
if (modeType == 2) {
GenerateRecord.value!.changeActive(true)
} else {
return
// 王斌 这块先注释
if (hbTopToolHtml && hbTopToolHtml.value) {
hbTopToolHtml.value!.closeBrush()
drawFree({ status: 0 })
}
}
}
//渲染底图
const loadImageAndRect = (imgUrl) => {
console.log(imgUrl)
state.baseMapUrl = imgUrl
// 定义裁剪区域
canvas.clipPath = new fabric.Rect({
originX: 'center',
originY: 'center',
left: state._cw / 2,
top: state._ch / 2 - state.away_top,
width: state._w,
height: state._h
})
fabric.Image.fromURL(imgUrl, { crossOrigin: 'anonymous' }).then((img) => {
// 设置图片的位置和大小
img.set({
id: 'hbImg',
objectCaching: false,
originX: 'center',
originY: 'center',
left: state._cw / 2,
top: state._ch / 2 - state.away_top, // 图片的顶坐标
width: state._w,
height: state._h,
selectable: false, // 禁用选择
evented: false // 禁用事件
// scaleX: 0.2, // 缩放比例
// scaleY: 0.2,
})
// 添加图片到画布
canvas.add(img)
//img.moveTo(1)
canvas.moveObjectTo(img, 1)
// canvas.setActiveObject(img)
})
}
// 修改控制点
const modifyControls = (canvasDom) => {
/* canvasDom.setControlsVisibility({
// tl: false, // 移除左上角控制点
// tr: false, // 移除右上角控制点
// bl: false, // 移除左下角控制点
// br: false, // 移除右下角控制点
// mt: false, // 移除中上控制点
// mb: false, // 移除中下控制点
// ml: false, // 移除中左控制点
// mr: false, // 移除中右控制点
// mtr: false
}) */
// console.log('???', whiteBoardRect.controls)
// whiteBoardRect.controls = fabric.util.object.clone(fabric.Object.prototype.controls);
canvasDom.controls.mtr.cursorStyle = 'pointer'
canvasDom.controls.mtr.offsetY = -25
canvasDom.controls.mtr.withConnection = false
// 遍历所有控制点并替换渲染方法
Object.keys(canvasDom.controls).forEach((key) => {
const control = canvasDom.controls[key]
// 关键步骤:克隆原型链中的controls配置,切断共享引用
// control.render = diamondControlRenderer
control.name = key
if (key === 'mt' || key === 'mb' || key === 'ml' || key === 'mr' || key === 'mtr') {
control.render = diamondControlRenderer
}
})
}
// 1. 自定义控制点渲染函数(带菱形和图标)
const diamondControlRenderer = function (ctx, left, top, styleOverride, fabricObject) {
const size = 12 // 控制点大小
const radius = 4 // 半圆半径(高度固定为size)
ctx.save()
ctx.translate(left, top) // 将原点移动到控制点中心
ctx.rotate(((fabricObject.angle || 0) * Math.PI) / 180) // 旋转控制点,使其与对象保持一致
// --- 绘制菱形 ---
if (this.name === 'mt' || this.name === 'mb' || this.name === 'ml' || this.name === 'mr') {
// --- 绘制胶囊形状 ---
ctx.beginPath()
if (this.name === 'mt' || this.name === 'mb') {
// 左侧半圆
ctx.arc(-size / 2, 0, radius, Math.PI / 2, (Math.PI * 3) / 2, false)
// // 右侧半圆
ctx.arc(size / 2, 0, radius, -Math.PI / 2, Math.PI / 2, false)
} else {
// 上半圆(从π到0,逆时针)
ctx.arc(0, -size / 2, radius, Math.PI, 0, false)
// 下半圆(从0到π,逆时针)
ctx.arc(0, size / 2, radius, 0, Math.PI, false)
}
ctx.closePath()
ctx.fillStyle = '#FFFFFF'
ctx.strokeStyle = '#FFFFFF'
ctx.lineWidth = 1
ctx.fill()
ctx.stroke()
}
// --- 绘制内部图标 ---
if (this.name == 'mtr') {
let size = 25
ctx.save()
ctx.translate(0, 0)
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle + 0))
ctx.drawImage(cachedImage, -size / 2, -size / 2, size, size)
ctx.restore()
}
ctx.restore()
}
const back = () => {
if (route.path == '/hb/make') {
setStorage('sourceImageId', '')
let currentPosterId = getStorage('currentPosterId')
let params = {
posterId: currentPosterId,
step: 0
}
updatePoster(params).then((res) => {
if (res && res.code == 200) {
push({
path: '/hb/generation'
})
}
})
}
}
/**
*left页面通过图片点击使用调用
*/
const userImage = (object) => {
fabric.Image.fromURL(object.imageUrl, { crossOrigin: 'anonymous' }).then(
(img) => {
// 设置图片的位置和大小
img.set({
left: state._cw / 2, // 图片的左坐标
top: state._ch / 2 - state.away_top, // 图片的顶坐标
width: object.width,
height: object.height,
originX: 'center',
originY: 'center',
selectable: true, // 禁用选择
evented: true, // 禁用事件
data: {
imageId: object.imageId,
imageUrl: object.imageUrl,
effect: 0
},
showIndex: canvas.getObjects().length
})
// 添加图片到画布
canvas.add(img)
},
{ crossOrigin: 'anonymous' }
)
}
/**
*通过历史记录的图片点击添加
*/
const addDTImgToCanva = (item) => {
if (canvas && canvas.openDrawingMode) {
//自由绘制时不允许添加
return
}
console.log(item.object)
if (!item.object.imageUrl) return
state._w = item.object.width
state._h = item.object.height
canvas.clipPath.left = state._cw / 2
canvas.clipPath.top = state._ch / 2 - state.away_top
canvas.clipPath.width = state._w
canvas.clipPath.height = state._h
canvas.getObjects().forEach(function (obj) {
// 检查对象的某些属性,例如 type, id 或其他自定义属性
if (obj.type === 'image' && obj.id === 'hbImg') {
console.log('找到了对象:', obj)
obj
.setSrc(item.object.imageUrl, {
crossOrigin: 'anonymous',
width: state._w,
height: state._h
})
.then(() => {
console.log('为什么进不来')
canvas.renderAll()
})
// setTimeout(() => {
// canvas.renderAll();
// canvas.requestRenderAll();
// },100)
}
})
//更新缓存数据
let hbMessage = getStorage('hbMessage')
state.baseMapUrl = item.object.imageUrl
hbMessage.imageUrl = item.object.imageUrl
hbMessage.imageId = item.object.imageId
hbMessage.backgroundImageSize.width = item.object.width
hbMessage.backgroundImageSize.height = item.object.height
setStorage('hbMessage', hbMessage)
state.baseMap.minWidth = state._w = item.object.width
state.baseMap.changeW = state._w //+ 200
state.baseMap.minHeight = state._h = item.object.height
state.baseMap.changeH = state._h //+ 100
// loadImageAndRect(result.data.backgroundImageUrl)
setStorage('sourceImageId', item.object.imageId)
setStorage('sourceImageUrl', item.object.imageUrl)
let currentPosterId = getStorage('currentPosterId')
let params = {
posterId: currentPosterId,
backgroundImageId: item.object.imageId,
backgroundImageUrl: item.object.imageUrl
}
updatePoster(params)
}
/**
*通过历史记录的素材点击添加
*/
const addImgToCanva = (item) => {
console.log(canvas.getObjects().length)
if (canvas && canvas.openDrawingMode) {
//自由绘制时不允许添加
return
}
fabric.Image.fromURL(item.object.imageUrl, { crossOrigin: 'anonymous' }).then(
(img) => {
// 设置图片的位置和大小
img.set({
originX: 'center',
originY: 'center',
left: state._cw / 2, // 图片的左坐标
top: state._ch / 2 - state.away_top, // 图片的顶坐标
width: item.object.width,
height: item.object.height,
selectable: true, // 禁用选择
evented: true, // 禁用事件
data: {
imageId: item.object.imageId,
imageUrl: item.object.imageUrl,
effect: 0
},
showIndex: canvas.getObjects().length,
cornerStyle: 'circle', // 控制点样式
cornerColor: 'white', // 控制点颜色
transparentCorners: false // 控制点不透明
})
modifyControls(img)
// 添加图片到画布
canvas.add(img)
},
{ crossOrigin: 'anonymous' }
)
canvas.renderAll()
}
/**
*抠图后添加图片
*/
const mattingAddImgToCanva = (item) => {
console.log(item)
fabric.Image.fromURL(item.imageUrl, { crossOrigin: 'anonymous' }).then(
(img) => {
// 设置图片的位置和大小
img.set({
originX: 'center',
originY: 'center',
left: state._cw / 2, // 图片的左坐标
top: state._ch / 2 - state.away_top, // 图片的顶坐标
width: item.width,
height: item.height,
selectable: true, // 禁用选择
evented: true, // 禁用事件
data: {
imageId: item.imageId,
imageUrl: item.imageUrl,
effect: 0
},
showIndex: canvas.getObjects().length
})
// 添加图片到画布
canvas.add(img)
},
{ crossOrigin: 'anonymous' }
)
canvas.renderAll()
}
/**
- 针对背景裁剪
*/
const tailorBoxRef = ref<HTMLElement>()
const cropBgCanvasImg = () => {
state.cropBg = true
state.centerDialogVisible = true
state.imagePath = state.baseMapUrl
state.oldInpimgW = state._w
state.oldInpimgH = state._h
setTimeout(() => {
state.boxWidth = tailorBoxRef.value!.offsetWidth
state.boxHeight = tailorBoxRef.value!.offsetHeight
}, 100)
}
/**
- 针对图片裁剪
*/
const cropCanvasImg = () => {
let objects = canvas.getActiveObjects()
if (objects.length > 1) {
return
}
let object = objects[0]
if (object.type != 'image') {
return
}
console.log(object.data)
state.chooseCanvasObject = object
state.centerDialogVisible = true
state.imagePath = object.data.imageUrl
state.oldInpimgW = object.width
state.oldInpimgH = object.height
setTimeout(() => {
state.boxWidth = tailorBoxRef.value!.offsetWidth
state.boxHeight = tailorBoxRef.value!.offsetHeight
}, 100)
}
/**
- 下载素材
*/
const downloadMaterial = async () => {
let objects = canvas.getActiveObjects()
if (objects.length > 1) {
return
}
let object = objects[0]
if (object.type != 'image') {
return
}
try {
// 发起请求获取图片数据
const response = await fetch(object.data.imageUrl)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
// 读取响应体为 Blob 对象
const blob = await response.blob()
// 创建一个临时的 URL 对象
const url = URL.createObjectURL(blob)
// 创建一个 <a> 元素
const a = document.createElement('a')
a.href = url
// 设置下载的文件名
// a.download = 'downloaded_image.png'
a.download = object.data.imageId + '.png'
// 模拟点击 <a> 元素来触发下载
a.click()
// 释放临时的 URL 对象
URL.revokeObjectURL(url)
} catch (error) {
ElMessage.error('下载图片时出错')
}
}
/**
- 针对图片 抠图
*/
const mattingCanvasImg = () => {
let objects = canvas.getActiveObjects()
if (objects.length > 1) {
return
}
let object = objects[0]
if (object.type != 'image') {
return
}
state.mattingDialogVisible = true
requestAnimationFrame(() => {
clearDrawing()
})
let _w = object.width > 1920 ? 1800 : 900
let _h = object.width > 1920 ? 720 : 360
let bWidth = object.width
let bHeight = object.height
if (bWidth / _w > bHeight / _h) {
state.imageWidth = bWidth / (bWidth / _w)
state.imageHeight = bHeight / (bWidth / _w)
} else if (bWidth / _w < bHeight / _h) {
state.imageWidth = bWidth / (bHeight / _h)
state.imageHeight = bHeight / (bHeight / _h)
}
// state.imageHeight = object.height;
// state.imageWidth = object.width;
//这里开始初始化canvas
requestAnimationFrame(() => {
//父调子
initMyCanvas(object)
})
}
const initMyCanvas = (object) => {
state.iamgeInitUrl = object.data.imageUrl
state.imageInitId = object.data.imageId
let canvas_ = document.getElementById('myCanvas') as HTMLCanvasElement
let container = document.getElementById('container') as HTMLCanvasElement
state.initCtx = canvas_.getContext('2d')
const canvasWidth = container.clientWidth - 32
const canvasHeight = container.clientHeight - 450
canvas_.width = state.imageWidth || canvasWidth
canvas_.height = state.imageHeight || canvasHeight
canvas_.addEventListener('click', (event: MouseEvent) => {
// 计算鼠标点击位置相对于canvas的坐标
const rect = canvas_.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
// 添加新点到数组
state.points.push({ x, y })
// 清除画布并重新绘制所有点
state.initCtx.clearRect(0, 0, canvas_.width, canvas_.height)
state.points.forEach((point) => {
state.initCtx.beginPath()
state.initCtx.arc(point.x, point.y, 5, 0, 2 * Math.PI)
state.initCtx.fillStyle = 'white'
state.initCtx.fill()
})
})
}
//生成图片
const createObjSeg = () => {
if (state.points.length < 1) {
return ElMessage.warning('请先绘制')
}
// state.loading = true
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement
//将canvas内容转换为Blob对象
canvas.toBlob(async (blob: Blob) => {
if (!blob) {
ElMessage('上传失败')
// state.loading = false
return
}
// 使用Blob对象创建File对象
const file = new File([blob], 'image.png', { type: 'img/png' })
const formData = new FormData()
formData.append('file', file)
//上传绘制的图片
/* const url =
import.meta.env.VITE_API_BASE_PATH === '/api'
? 'http://localhost:5000/api/scene/upload'
: `${import.meta.env.VITE_API_BASE_PATH}/scene/upload` */
const url = window.location.origin + '/api/v2/upload/images'
let res = await fetch(url, {
method: 'POST', // 或者使用 'post'(不区分大小写)
body: formData
})
.then((response) => {
return response.json()
})
.catch(() => {
ElMessage('上传失败')
// state.loading = false
})
.finally(() => {})
if (res.code == 200) {
state.gloLoading = true
state.loadingText = '抠图中...'
//上传成功后生成图片
matting({
sourceImageId: state.imageInitId, //必须原图
maskImageId: res.data.imageId // 必需 打点遮罩图
})
.then((segRes) => {
if (segRes.code == 200) {
// state.imageInitId = ''
// state.iamgeInitUrl = ''
// state.mattingDialogVisible = false;
state.mattingObjectUrl = segRes.data.imageUrl
state.mattingDialogVisible_Second = true
state.mattingGenerate = segRes.data
// refImageUrl(segRes.data);
clearDrawing()
} else {
ElMessage('生成失败')
}
})
.catch(() => {
ElMessage('生成失败')
})
.finally(() => {
// state.loading = false
state.gloLoading = false
})
} else {
// state.loading = false
}
})
}
const refMatting = () => {
refImageUrl(state.mattingGenerate)
state.mattingDialogVisible = false
state.mattingDialogVisible_Second = false
}
//清除绘制
const clearDrawing = () => {
state.points = []
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement
canvas.width = canvas.width
}
const base64 = ref('')
const cropperExpose = ref<InstanceType<typeof ImageCropping>>()
const getBase64 = () => {
state.gloLoading = true
state.loadingText = '导出下载中...'
base64.value = unref(cropperExpose)?.cropperExpose?.getCroppedCanvas()?.toDataURL() ?? ''
const formData = new FormData()
let blod = base64ToBlob(base64.value, 'image/png')
console.log(URL.createObjectURL(blod))
const file = new File([blod], 'fileName', { type: 'image/png' })
getResolutionFromImage(file, async (width: number, height: number) => {
// 当图片大小超过2048*2048,进行压缩
if (width > 2048 || height > 2048) {
const compressionFile = await compressImage(file)
// 计算压缩后的图片大小
const { newWidth, newHeight } = calculateScaledDimensions(width, height)
fileCue(formData, compressionFile, newWidth, newHeight)
} else if (width < 256 || height < 256) {
ElMessage.error('图片分辨率不能小于256*256')
state.gloLoading = false
} else if ((width >= 256 && width <= 2048) || (height >= 256 && height <= 2048)) {
formData.append('file', file) // 将文件添加到FormData对象中
fileUpload(formData)
}
})
}
// 文件接口
const fileUpload = (formData: any) => {
const url = window.location.origin + '/api/v2/upload/images'
fetch(url, {
method: 'POST', // 或者使用 'post'(不区分大小写)
body: formData
})
.then((response) => {
if (!response.ok) {
ElMessage.error('网络响应不是ok状态')
}
return response.json() // 如果需要,可以返回解析后的JSON数据
})
.then((data) => {
console.log('我上传成功了')
state.centerDialogVisible = false
state.imagePath = ''
state.baseMapUrl = data.data.imageUrl
if (state.cropBg) {
let hbMessage = getStorage('hbMessage')
hbMessage.imageUrl = data.data.imageUrl
hbMessage.imageId = data.data.imageId
hbMessage.backgroundImageSize.width = data.data.width
hbMessage.backgroundImageSize.height = data.data.height
setStorage('hbMessage', hbMessage)
setStorage('sourceImageId', data.data.imageId)
setStorage('sourceImageUrl', data.data.imageUrl)
state.cropBg = false
state.baseMap.minWidth = state._w = data.data.width
state.baseMap.minHeight = state._h = data.data.height
canvas.clipPath.left = state._cw / 2
canvas.clipPath.top = state._ch / 2 - state.away_top
canvas.clipPath.width = state._w
canvas.clipPath.height = state._h
canvas.getObjects().forEach(function (obj) {
// 检查对象的某些属性,例如 type, id 或其他自定义属性
if (obj.type === 'rect' && obj.id === 'hsc') {
obj.left = state._cw / 2
obj.top = state._ch / 2 - state.away_top
obj.width = state._w
obj.height = state._h
canvas.renderAll()
}
if (obj.type === 'image' && obj.id === 'hbImg') {
obj.setSrc(data.data.imageUrl, { crossOrigin: 'anonymous' }).then(() => {
obj.left = state._cw / 2
obj.top = state._ch / 2 - state.away_top
obj.width = state._w
obj.height = state._h
canvas.renderAll()
})
}
})
let currentPosterId = getStorage('currentPosterId')
let params = {
posterId: currentPosterId,
backgroundImageId: data.data.imageId,
backgroundImageUrl: data.data.imageUrl
}
updatePoster(params)
let param_ = {
posterId: currentPosterId,
type: 1,
imageList: [data.data.imageId]
}
addGenerateRecords(param_).then((res) => {
if (res.code === 200) {
GenerateRecord.value!.refGenerImg()
}
})
} else {
refImageUrl(data.data)
}
})
.catch(() => {
ElMessage.error('上传失败')
})
.finally(() => (state.gloLoading = false))
}
/**
- 更新图片组件的显示内容
*/
const refImageUrl = (data) => {
console.log(data)
let object = canvas.getActiveObject()
canvas.discardActiveObject()
if (object.type != 'image') {
return
}
console.log(object.data)
let objectData = object.data
if (objectData == null) {
object.set('data', {
imageUrl: data.imageUrl,
imageId: data.imageId
})
} else {
objectData.imageUrl = data.imageUrl
objectData.imageId = data.imageId
object.set('data', objectData)
}
state.imagePath = data.imageId
object.setSrc(data.imageUrl, { crossOrigin: 'anonymous' }).then(() => {
console.log('123')
canvas.setActiveObject(object)
canvas.requestRenderAll()
})
let currentPosterId = getStorage('currentPosterId')
let param_ = {
posterId: currentPosterId,
type: 2,
imageList: [data.imageId]
}
addGenerateRecords(param_).then((res) => {
if (res.code === 200) {
GenerateRecord.value!.refGenerList()
}
})
}
const fileCue = (formData: any, compressionFile: any, newWidth: number, newHeight: number) => {
ElMessageBox.confirm(该图片超过2K,将等比缩放图片压缩至${newWidth}*${newHeight}
, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
.then(() => {
formData.append('file', compressionFile) // 将文件添加到FormData对象中
fileUpload(formData)
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消上传'
})
})
}
// 图片压缩
const compressImage = (file: File) => {
return new Promise((resolve, reject) => {
new Compressor(file, {
// 压缩质量,0-1之间。数字越小,压缩比越高
quality: 0.6,
width: 2048,
height: 2048,
success(result) {
// 默认返回result是blob类型的文件,可以转成file并返回,交给afterRead钩子进行上传
let newFile = new File([result], file.name, { type: file.type })
resolve(newFile)
},
error(err) {
reject(err)
}
})
})
}
// 图片分辨率
const getResolutionFromImage = (file, callback) => {
var reader = new FileReader()
reader.onload = function (e) {
var img = new Image()
img.onload = function () {
callback(img.width, img.height)
}
img.src = e.target?.result as string // 设置 img.src
}
reader.readAsDataURL(file)
}
const openPoster = async (id, oldId) => {
//切换场景前先保存一次
if (oldId != null) {
await saveData(oldId)
}
// 获取海报资产信息
getPosterInfo({ posterId: id }).then((result) => {
if (result && result.code == 200) {
canvas?.clear()
state.generateName = result.data.name
setStorage('currentPosterName', result.data.name)
setStorage('hbMessage', result.data)
setStorage('sourceImageId', result.data.backgroundImageId)
setStorage('sourceImageUrl', result.data.backgroundImageUrl)
state.baseMap.minWidth = state._w = result.data.backgroundImageSize.width
state.baseMap.minHeight = state._h = result.data.backgroundImageSize.height
loadImageAndRect(result.data.backgroundImageUrl)
nextTick(() => {
// state.draggableResizable = true
generateListHtml.value!.updateGenerateName({ name: state.generateName })
requestAnimationFrame(() => {
Promise.all(fonts.map((font) => font.load())).then((loadedFonts) => {
// 将所有加载的字体添加到 `document.fonts`
loadedFonts.forEach((font) => document.fonts.add(font))
document.fonts.ready.then(() => {
initTextObject(JSON.parse(result.data.metaData) || [])
})
})
})
})
}
})
}
/**
- base64转bold
*/
const base64ToBlob = (base64, mimeType) => {
// 提取纯 Base64 数据(去掉前缀)
const base64Data = base64.split(',')[1]
// 将 Base64 字符串转换为二进制数据
const byteCharacters = atob(base64Data) // 解码 Base64
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i) // 转换为字节数组
}
// 创建 Blob 对象
const byteArray = new Uint8Array(byteNumbers)
return new Blob([byteArray], { type: mimeType })
}
//修改海报名称
const updateName = (param) => {
let currentPosterId = getStorage('currentPosterId')
if (currentPosterId) {
state.generateName = param.name
setStorage('currentPosterName', state.generateName)
let params = {
posterId: currentPosterId,
name: param.name
}
renamePosters(params)
queryData()
}
}
/**
- logo上传成功后的事件
*/
const uploadImage = (data) => {
console.log('进来了')
let ids: any = []
ids.push(data.imageId)
let currentPosterId = getStorage('currentPosterId')
let param_ = {
posterId: currentPosterId,
type: 2,
imageList: ids
}
addGenerateRecords(param_).then((res) => {
if (res.code === 200) {
GenerateRecord.value!.refGenerList()
}
})
fabric.Image.fromURL(data.imageUrl, { crossOrigin: 'anonymous' }).then(
(img) => {
// 设置图片的位置和大小
img.set({
left: state._cw / 2, // 图片的左坐标
top: state._ch / 2 - state.away_top, // 图片的顶坐标
width: data.width,
height: data.height,
selectable: true, // 禁用选择
evented: true, // 禁用事件
data: {
imageId: data.imageId,
imageUrl: data.imageUrl,
effect: 0
},
originX: 'center',
originY: 'center',
showIndex: canvas.getObjects().length
})
modifyControls(img)
// 添加图片到画布
canvas.add(img)
},
{ crossOrigin: 'anonymous' }
)
canvas.renderAll()
}
/**
- 获取添加的文件的option
*/
const getTextOption = () => {
let index = canvas.getObjects().length
console.log(index)
return {
left: state._cw / 2, // 文本框的 X 坐标
top: state._ch / 2 - state.away_top, // 文本框的 Y 坐标
width: 300, // 文本框的宽度
fontSize: 42, // 字体大小
borderColor: 'white', // 边框颜色
fill: 'white', // 文本颜色
hasControls: true, // 允许调整大小和旋转
hasBorders: true, // 显示边框
textAlign: 'left', // 文本对齐方式
// editingBorderColor: 'red',
fontFamily: 'huiwenfangsong',
splitByGrapheme: true,
cornerStyle: 'circle', // 控制点样式
cornerColor: 'white', // 控制点颜色
transparentCorners: false, // 控制点不透明
lockScalingFlip: false, // 允许翻转
lockUniScaling: false, // 允许非等比缩放
selectable: true, // 确保对象可选择
evented: true, // 确保对象可交互
data: {
effect: 0
},
shadow: {},
stroke: '',
originX: 'center',
originY: 'center',
showIndex: index
}
}
/**
*
- @param type 艺术字
*/
const generateText = (type) => {
let options = getTextOption()
const placeholderText = '请输入文字...'
if (type == 0) {
options.fontFamily = 'Slidefu-Regular'
options.fill = '#FFFFFF'
} else if (type == 1) {
options.fontFamily = 'FeiboZhengdianBody'
options.fill = '#FFFFFF'
} else if (type == 2) {
options.fontFamily = 'Optimal-Title'
options.fill = '#FFFFFF'
options.shadow = {
color: '#0DE41C', // 阴影颜色
blur: 5, // 模糊半径
offsetX: 2, // 水平偏移
offsetY: 2 // 垂直偏移
}
} else if (type == 3) {
options.fontFamily = 'SmileySans-Oblique'
options.fill = new fabric.Gradient({
type: 'linear',
gradientUnits: 'pixels', // 或 'percentage'
coords: { x1: 0, y1: 0, x2: 0, y2: 50 }, // 水平渐变
colorStops: [
{ offset: 0, color: '#EFFEC9' },
{ offset: 1, color: '#6FFEE4' }
]
})
} else if (type == 4) {
options.fontFamily = 'Fragrant-comfortable'
options.fill = '#FFFFFF'
options.stroke = '#FDDD28'
options.shadow = {}
}
let textbox = new fabric.Textbox(placeholderText, options)
textbox.setControlVisible('mt', false)
textbox.setControlVisible('mb', false)
textbox.selectionStart = textbox.text.length
textbox.selectionEnd = textbox.text.length
modifyControls(textbox)
canvas.add(textbox)
canvas.setActiveObject(textbox)
textbox.enterEditing()
// 重新渲染画布
canvas.renderAll()
}
const exportImage = throttle(
function () {
console.log('11111')
state.gloLoading = true
state.loadingText = '导出下载中...'
let objects = canvas.getObjects()
let activity = canvas.getActiveObjects()
canvas.discardActiveObject()
let type = 0
for (var i = 0; i < objects.length; i++) {
if (objects[i].data != null && objects[i].data.effect != null) {
let effect = objects[i].data.effect
if (objects[i].type == 'image') {
//图片
console.log(effect)
if (effect == 1) {
type = 1
//淡入淡出
imgFadeEffec(objects[i])
}
if (effect == 2) {
type = 1
//旋转
imgRotate(objects[i])
}
if (effect == 3) {
type = 1
//缩放
imgScale(objects[i])
}
if (effect == 4) {
type = 1
//翻转
imgFlip(objects[i])
}
if (effect == 5) {
type = 1
//抖动
imgShake(objects[i])
}
} else if (textObject.includes(objects[i].type)) {
//文字
if (effect == 1) {
type = 1
fadeEffec(objects[i])
}
if (effect == 2) {
type = 1
colorFillFun(objects[i])
}
if (effect == 3) {
type = 1
rotateText(objects[i])
}
if (effect == 4) {
type = 1
scale(objects[i])
}
if (effect == 5) {
type = 1
movePosition(objects[i])
}
}
}
}
if (type == 1) {
capturerFun(activity)
} else {
captureImg(activity)
}
},
1000,
{ trailing: false }
)
//撤销画笔
const revoke = () => {
state.points.pop()
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement
// 清除画布并重新绘制所有点
state.initCtx.clearRect(0, 0, canvas.width, canvas.height)
state.points.forEach((point) => {
state.initCtx.beginPath()
state.initCtx.arc(point.x, point.y, 5, 0, 2 * Math.PI)
state.initCtx.fillStyle = '#ffffff'
state.initCtx.fill()
})
}
/**
- 关闭裁剪
*/
const closeHeading = () => {
state.centerDialogVisible = false
state.cropBg = false
}
/**
*
- @param param 点击立即生成调用
*/
const onGenerates = async (param) => {
try {
// if (param.imgaeId == null) {
// param.imageId = '1'
// }
generateMaterials(param)
.then((res) => {
if (res.code === 200) {
leftToolHtml.value!.refImageList(res.data)
let ids: any = []
res.data.forEach((element) => {
ids.push(element.imageId)
})
let currentPosterId = getStorage('currentPosterId')
let param_ = {
posterId: currentPosterId,
type: 2,
imageList: ids
}
addGenerateRecords(param_).then((res) => {
if (res.code === 200) {
GenerateRecord.value!.refGenerList()
}
})
}
})
.finally(() => {
hbCreateRef.value!.resetLoading()
})
} catch {}
}
</script>
<template>
<div class="hb-module" id="hb-module" ref="canvasContainerRef">
<div class="to-back cursor-pointer" v-show="state.parentResultType==0">
<div @click="back">
<el-icon>
<ArrowLeftBold />
</el-icon>
<span class="ml-5px mb-2px">返回</span>
</div>
</div>
<div class="canvas-container">
<canvas id="canvasContainer" ref="canvasRef"></canvas>
<!-- 顶部工具栏 -->
<section class="absolute-top20-left-50">
<hbTopTool ref="hbTopToolHtml" @addFonts="addText" @setDirection="setDirection" @setColors="updateTextColor" @setBrushColor="updateBrushColor" @setBrushSize="updateBrushSize"
@setBrushOpacity="updateBrushOpacity" @setFontBold="updateTextFontBold" @setFontSize="updateTextFontSize" @setFontPosition="updateTextFontPosition" @setFontEffect="setFontEffect"
@imgDynamicEffect="imgDynamicEffect" @changeFontFamily="updateFontFamily" :modeType="state.parentModeType" @draw="drawFree" @undo="brushUndo" @clear="brushClear" @cropImg="cropCanvasImg"
@cropBgImg="cropBgCanvasImg" @mattingCanvasImg="mattingCanvasImg" @downloadMaterial="downloadMaterial" @uploadImage="uploadImage" @enlargeMap="enlargeMap"
v-show="state.parentModeType!=0&&(!state.brushComplete||state.parentModeType!=2)">
</hbTopTool>
</section>
<!-- 左侧工具栏 -->
<section class="absolute-left20-left-50" v-show="state.parentResultType==0">
<leftTool ref="leftToolHtml" @toolMode="toolModes" @userImage="userImage"></leftTool>
</section>
<!-- 右侧历史记录 -->
<section class="hc-re">
<generateRecord ref="GenerateRecord" @addImgToCanvas="addImgToCanva" @addDTImgToCanvas="addDTImgToCanva" v-show="state.parentResultType==0"></generateRecord>
<!-- 右下侧缩放 -->
<section class="hc-zoom" :msg="state.zoom">
<proportionalValue :zoom="state.zoom" v-show="state.parentResultType==0"></proportionalValue>
</section>
</section>
</div>
<section class="absolute-bottom20-left-50">
<!-- 底部工具栏 -->
<hbBottomTool :modeType="state.parentModeType" v-if="state.parentModeType!=0" :resultType="state.parentResultType" @clearCommit="clearCommit" @reClear="reClear" :drawnPaths="state.drawnPaths"
:brushStatus="state.brushStatus" @reEdit="reEdit" @complete="complete" @generateText="generateText">
</hbBottomTool>
<!-- 底部工具栏 素材 生成 -->
<hbCreate :btnText="state.btnText" v-if="state.parentModeType==3" @onGenerate="onGenerates" ref="hbCreateRef" :contentPlaceholder="state.contentPlaceholder"></hbCreate>
</section>
<generateList :generateName="state.generateName" :lastPosters="state.lastPosters" @openPosters="openPoster" @updateName="updateName" ref="generateListHtml" v-show="state.parentResultType==0">
</generateList>
<!-- 底图裁剪和素材裁剪 -->
<div v-if="state.centerDialogVisible" class="glo-loading">
<div class="dialog-draggable-resizable" v-loading="state.gloLoading" element-loading-custom-class="htm-custom-loading" element-loading-spinner="el-loading-spinner"
element-loading-background="rgba(0, 0, 0, 0.6)" :element-loading-text="state.kuotuTextloading">
<div class="heading">
<span class="heading__title">裁剪</span>
<span class="heading__close" @click="closeHeading"></span>
</div>
<div class="caijian" ref="tailorBoxRef" v-loading="state.gloLoading" element-loading-custom-class="htm-custom-loading" element-loading-spinner="el-loading-spinner"
element-loading-background="rgba(0, 0, 0, 0.6)" element-loading-text="裁剪中...">
<ImageCropping ref="cropperExpose" :image-url="state.imagePath" :boxWidth="state.boxWidth" :boxHeight="state.boxHeight" :oldInpimgW="state.oldInpimgW" :oldInpimgH="state.oldInpimgH" />
</div>
<div class="dialog-footer2">
<span class="h-btn btn-cancel" @click="state.centerDialogVisible = false">
取消
</span>
<span class="h-btn btn-confirm" @click="getBase64">
确认
</span>
</div>
</div>
</div>
<!-- 抠图弹窗 -->
<div v-if="state.mattingDialogVisible" class="glo-loading">
<div class="dialog-draggable-resizable">
<div class="heading">
<span class="heading__title">抠图</span>
<span class="heading__close" @click="state.mattingDialogVisible=false"></span>
<div class="btn-options">
<el-tooltip effect="dark" content="撤销打点" placement="bottom">
<div class="btn-img">
<span class="revoke-piont" @click="revoke"></span>
</div>
</el-tooltip>
<el-tooltip effect="dark" content="清空打点" placement="bottom">
<div class="btn-img">
<span class="clear-piont" @click="clearDrawing"></span>
</div>
</el-tooltip>
</div>
</div>
<div ref="mattingRef" class="caijian image-matting">
<div id="container" class="image" :style="{'background': `url(${state.iamgeInitUrl}) center center no-repeat`,width:state.imageWidth+'px',height:state.imageHeight+'px'}">
<canvas class="myCanvas" id="myCanvas" :style="{ width: state.imageWidth + 'px', height: state.imageHeight + 'px' }" :msg="state.imageWidth + '??' + state.imageHeight"></canvas>
</div>
</div>
<div class="dialog-footer2">
<span class="h-btn btn-cancel" @click="state.mattingDialogVisible = false">
取消
</span>
<span class="h-btn btn-confirm" @click="createObjSeg">
确认
</span>
</div>
</div>
</div>
<!-- <el-dialog v-model="state.mattingDialogVisible" title="抠图" destroy-on-close center width="65%">
<div ref="mattingRef" class="image-matting" style="height:65vh;">
<div id="container" class="image" :style="{'background': `url(${state.iamgeInitUrl}) center center no-repeat`,width:state.imageWidth+'px',height:state.imageHeight+'px'}">
<canvas class="myCanvas" id="myCanvas" :style="{ width: state.imageWidth + 'px', height: state.imageHeight + 'px' }" :msg="state.imageWidth + '??' + state.imageHeight"></canvas>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="state.mattingDialogVisible = false">取消</el-button>
<el-button type="primary" @click="createObjSeg">
确认
</el-button>
<el-button @click="revoke">
撤销打点
</el-button>
<el-button @click="clearDrawing">
清空打点
</el-button>
</div>
</template>
</el-dialog> -->
<!-- 抠图 是否应用? -->
<div v-if="state.mattingDialogVisible_Second" class="glo-loading">
<div class="dialog-draggable-resizable">
<div class="heading">
<span class="heading__title">是否应用?</span>
<span class="heading__close" @click="state.mattingDialogVisible_Second=false"></span>
</div>
<div class="caijian image-matting">
<div id="container" class="image" :style="{ 'background-image': `url(${state.mattingObjectUrl})`,width:state.imageWidth+'px',height:state.imageHeight+'px'}">
<canvas class="myCanvas" id="myCanvas" :style="{ width: state.imageWidth + 'px', height: state.imageHeight + 'px' }" :msg="state.imageWidth + '??' + state.imageHeight"></canvas>
</div>
</div>
<div class="dialog-footer2">
<span class="h-btn btn-cancel" @click="state.mattingDialogVisible_Second = false">
撤销
</span>
<span class="h-btn btn-confirm" @click="refMatting">
确认
</span>
</div>
</div>
</div>
<!-- <el-dialog v-model="state.mattingDialogVisible_Second" title="是否应用?" destroy-on-close center width="65%">
<div class="image-matting" style="height:65vh;">
<div id="container" class="image" :style="{ 'background-image': `url(${state.mattingObjectUrl})`,width:state.imageWidth+'px',height:state.imageHeight+'px'}">
<canvas class="myCanvas" id="myCanvas" :style="{ width: state.imageWidth + 'px', height: state.imageHeight + 'px' }" :msg="state.imageWidth + '??' + state.imageHeight"></canvas>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="state.mattingDialogVisible_Second = false">撤销</el-button>
<el-button type="primary" @click="refMatting">
确认
</el-button>
</div>
</template>
</el-dialog> -->
<!-- 扩图 -->
<div v-if="state.dialogdraggableResizable" class="glo-loading">
<div class="dialog-draggable-resizable" v-loading="state.loading2" element-loading-custom-class="htm-custom-loading" element-loading-spinner="el-loading-spinner"
element-loading-background="rgba(0, 0, 0, 0.6)" :element-loading-text="state.kuotuTextloading">
<div class="heading">
<span class="heading__title">{{state.kuotuText}}</span>
<span class="heading__close" @click="state.dialogdraggableResizable=false;state.contrastImgStatus = false;state.kuotuText = '扩图'"></span>
</div>
<div class="parent-draggable-resizable" ref="parentDragResizRef">
<EnlargeSceneMatch v-show="!state.contrastImgStatus" ref="enlargeSceneMatchRef"></EnlargeSceneMatch>
<!-- 扩图对比 -->
<contrastImg2 v-show="state.contrastImgStatus" ref="contrastImgRef2"></contrastImg2>
</div>
<div class="h-bottom-resizable" v-show="!state.contrastImgStatus">
<div class="img-size">
<div class="scale-mode">
<div class="proportion" :class="{'proportion-bg':index==state.toolbarActive}" v-for="(item,index) in state.toolbar" :key="index" @click="switchingRatio(item,index)">
<span class="px-img2" :class="[`bl-${item.id}`]"></span> {{item.name}}
</div>
<el-button type="primary" :disabled="false" :loading="state.loading" @click="baseMapGeneration">立即生成</el-button>
</div>
</div>
</div>
<div class="dialog-footer2" v-show="state.contrastImgStatus">
<span class="h-btn btn-cancel" @click="state.contrastImgStatus = false;state.kuotuText = '扩图'">
取消
</span>
<span class="h-btn btn-confirm" @click="playGenerate">
确认
</span>
</div>
</div>
</div>
<div class="glo-loading" v-if="state.gloLoading" v-loading="state.gloLoading" element-loading-custom-class="htm-custom-loading" element-loading-spinner="el-loading-spinner"
element-loading-background="rgba(0, 0, 0, 0.7)" :element-loading-text="state.loadingText">
</div>
<!-- 动画透明遮罩 -->
<div :msg="state.gloLoading2" class="glo-loading2" v-if="state.gloLoading2">
</div>
<div :msg="state.parentResultType+'?'+state.guiding3" class="guiding-prompt3" v-show="state.parentResultType==1&&state.guiding3==1" @click="setguiding3">
<div class="guiding-prompt2" id="guiding3">
<div class="triangle-tip"></div>
<div class="centen">
<span class="txt1">
涂抹消除或修改
</span>
<span class="txt2">
将鼠标悬停在图片上,按住左键即可像握笔一样随心涂抹 ,可以涂抹图片中不需要的部分
</span>
</div>
</div>
</div>
<div class="guiding-prompt3" v-show="state.parentResultType==3&&state.guiding4==1" @click="setguiding4">
<div class="guiding-prompt2" id="guiding4">
<div class="triangle-tip"></div>
<div class="centen">
<span class="txt1">
涂抹消除或修改
</span>
<span class="txt2">
将鼠标悬停在图片上,按住左键即可像握笔一样随心涂抹 ,可以涂抹图片中不需要的部分
</span>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
{
user-select: none;
}
.hb-module {
width: 100%;
height: calc(100vh - 1px - var(--logo-height));
position: relative;
background: #0f1115;
overflow: hidden !important;
.canvas-container {
background: #0f1115;
position: relative;
}canvasContainer {
overflow: inherit !important;
border: 0 !important;
}
.hc-re {
position: absolute;
top: 20px;
right: 20px;
z-index: 8;
height: calc(100% - 20px);
min-height: calc(260px + 48px + 40px + 35px);
}
.hc-zoom {
position: absolute;
right: 20px;
bottom: 20px;
z-index: 8;
// bottom: 0px;
height: 35px;
}
.to-back {
position: absolute;
left: 10px;
top: 10px;
z-index: 8;
div {width: 74px; height: 40px; background: #1d2129; border-radius: 6px; display: flex; align-items: center; justify-content: center;
}
}
.image-matting {
display: flex;
align-items: center;
width: 100%;
position: relative;
justify-content: center;
.image {width: 100%; height: 500px; background-size: 100% 100% !important;
}
}
}
.dialog-draggable-resizable {
width: 80%;
height: 85%;
padding: 20px 20px 0 20px;
position: fixed;
left: 50%;
top: 50%;
z-index: 9999;
transform: translate(-50%, -50%);
background: #2a2e33;
border: 1px solid #484d53;
border-radius: 20px;
.heading {
display: flex;
justify-content: space-between;
align-items: center;
color: #fff;
font-size: 16px;
height: 28px;
margin-bottom: 20px;
.heading__close {background: url('../../assets/img/close3.png') no-repeat center; display: flex; width: 28px; height: 28px; background-size: 100% 100% !important; cursor: pointer; opacity: 0.7; &:hover { opacity: 1; }
}
.btn-options {position: absolute; z-index: 99; left: 50%; transform: translate(-50%, 0); display: flex; height: 40px; background: #1d2129; border-radius: 6px; display: flex; align-items: center; box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.25); padding: 4px; .btn-img { width: 32px; height: 32px; border-radius: 4px; &:hover { background: #323942; } span { cursor: pointer; display: flex; width: 32px; height: 32px; background-size: 20px 20px !important; } span + span { margin-left: 8px; } .revoke-piont { background: url('../../assets/img/cs.png') no-repeat center; } .clear-piont { background: url('../../assets/img/qkhb.png') no-repeat center; } }
}
}
.caijian {
width: 100%;
// 28+20+92
height: calc(100% - 140px);
position: relative;
}
// 28+40+90
.parent-draggable-resizable {
width: 100%;
height: calc(100% - 158px);
position: relative;
}
.h-bottom-resizable {
position: absolute;
bottom: 20px;
left: 50%;
transform: translate(-50%, 0);
.img-size {width: 664px; height: 90px; // background: #1d2129; border-radius: 10px; padding: 16px; .scale-mode { display: flex; .proportion { width: 64px; height: 58px; background: #404853; border-radius: 10px; margin-right: 8px; font-size: 12px; font-family: PingFang SC, PingFang SC-400; font-weight: 400; color: #ffffff; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; .px-img2 { display: flex; width: 20px; height: 20px; background-size: 100% 100% !important; } .bl-1 { background: url('../../assets/img/zyms.png') no-repeat center; } .bl-2 { background: url('../../assets/img/bl.png') no-repeat center; } .bl-3 { background: url('../../assets/img/1v1.png') no-repeat center; } .bl-4 { // 9:16 background: url('../../assets/img/bl-2.png') no-repeat center; } .bl-5 { // 16:9 background: url('../../assets/img/bl-3.png') no-repeat center; } .bl-6 { background: url('../../assets/img/bl-4.png') no-repeat center; } .bl-7 { background: url('../../assets/img/bl-5.png') no-repeat center; } } .proportion-bg { background: #696e76 !important; } .el-button { width: 128px; height: 58px; line-height: 58px; background: linear-gradient(90deg, #0066ff, #00b2ff 100%); border-radius: 10px; font-size: 14px; } }
}
}
.dialog-footer2 {
position: absolute;
bottom: 20px;
left: 50%;
transform: translate(-50%, 0);
display: flex;
justify-content: center;
align-items: center;
.h-btn {width: 72px; height: 40px; border-radius: 6px; font-size: 14px; color: #ffffff; display: flex; justify-content: center; align-items: center; cursor: pointer;
}
.h-btn + .h-btn {margin-left: 8px;
}
.btn-cancel {background: #394147; &:hover { background: #485157; }
}
.btn-confirm {background: #0066ff; box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.25); &:hover { background: #247bff; }
}
}
:deep(.htm-custom-loading) {
border-radius: 20px;
}
}
</style>
<style lang="less">
.my-handle-class {
position: absolute;
z-index: 99;
background-color: #fff;
border: none;
border-radius: 50%;
height: 14px;
width: 14px;
-webkit-transition: all 300ms linear;
-ms-transition: all 300ms linear;
transition: all 300ms linear;
}
.my-handle-class-tl {
top: -5px;
left: -7px;
cursor: nw-resize;
}
.my-handle-class-tm {
top: -5px;
cursor: n-resize;
border-radius: 4px;
width: 20px;
height: 8px;
left: 50%;
transform: translate(-50%, 0px);
}
.my-handle-class-tr {
top: -5px;
right: -7px;
cursor: ne-resize;
}
.my-handle-class-ml {
top: 50%;
left: -5px;
cursor: w-resize;
border-radius: 4px;
width: 8px;
height: 20px;
top: 50%;
transform: translate(0px, -50%);
}
.my-handle-class-mr {
top: 50%;
right: -5px;
cursor: w-resize;
border-radius: 4px;
width: 8px;
height: 20px;
top: 50%;
transform: translate(0px, -50%);
}
.my-handle-class-bl {
bottom: -5px;
left: -7px;
cursor: sw-resize;
}
.my-handle-class-bm {
bottom: -5px;
cursor: n-resize;
border-radius: 4px;
width: 20px;
height: 8px;
left: 50%;
transform: translate(-50%, 0px);
}
.my-handle-class-br {
bottom: -5px;
right: -7px;
cursor: se-resize;
}
.vdr {
position: absolute;
border: 1px solid #0066ff;
box-sizing: border-box;
}
.glo-loading {
position: fixed !important;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 99;
}
.glo-loading2 {
position: fixed !important;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 99;
cursor: not-allowed;
}
.guiding-prompt3 {
position: fixed; /* 切换为固定定位,脱离父级约束 */
left: 0;
top: 0;
right: 0;
bottom: 0; /* 四边撑满视口 */
width: 100%;
height: 100%;
z-index: 9999; /* 确保遮罩层位于页面最顶层 */
.guiding-prompt2 {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
</style>
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。