1358 lines
44 KiB
JavaScript
1358 lines
44 KiB
JavaScript
const {
|
||
ipcRenderer,
|
||
contextBridge,
|
||
ipcMain
|
||
} = require('electron')
|
||
|
||
const addEvent = function (el, type, fn) {
|
||
if (el.addEventListener)
|
||
el.addEventListener(type, fn, false);
|
||
else
|
||
el.attachEvent('on' + type, fn);
|
||
};
|
||
|
||
const extend = function (obj, ext) {
|
||
for (var key in ext)
|
||
if (ext.hasOwnProperty(key))
|
||
obj[key] = ext[key];
|
||
return obj;
|
||
};
|
||
|
||
const interact = require('./interact.min.js')
|
||
let state = {
|
||
mode: 'init', // 'init', 'standard', 'edit-video'
|
||
editVideo: {
|
||
|
||
},
|
||
currentScale: 1,
|
||
translate: {
|
||
translateX: 0,
|
||
translateY: 0
|
||
},
|
||
elements: [
|
||
|
||
],
|
||
workspaceRect: {
|
||
x1: 0,
|
||
y1: 0,
|
||
x2: 0,
|
||
y2: 0
|
||
},
|
||
isLocked: false
|
||
}
|
||
|
||
// 撤销/重做历史记录
|
||
let history = {
|
||
undoStack: [],
|
||
redoStack: [],
|
||
maxSize: 50
|
||
}
|
||
|
||
// 保存当前状态到历史记录
|
||
function saveStateToHistory() {
|
||
// 创建状态快照(深拷贝)
|
||
const snapshot = {
|
||
elements: state.elements.map(el => ({
|
||
path: el.path,
|
||
type: el.type,
|
||
x: parseFloat(el.element.dataset.x) || 0,
|
||
y: parseFloat(el.element.dataset.y) || 0,
|
||
width: el.width,
|
||
height: el.height,
|
||
loopPairs: el.loopPairs,
|
||
activeLoopPair: el.activeLoopPair
|
||
})),
|
||
currentScale: state.currentScale,
|
||
translate: { ...state.translate }
|
||
}
|
||
|
||
history.undoStack.push(snapshot)
|
||
|
||
// 限制历史记录大小
|
||
if (history.undoStack.length > history.maxSize) {
|
||
history.undoStack.shift()
|
||
}
|
||
|
||
// 清空重做栈
|
||
history.redoStack = []
|
||
|
||
console.log('State saved to history, undo stack size:', history.undoStack.length)
|
||
}
|
||
|
||
// 撤销
|
||
function undo() {
|
||
if (history.undoStack.length === 0) {
|
||
console.log('Nothing to undo')
|
||
return
|
||
}
|
||
|
||
// 保存当前状态到重做栈
|
||
const currentSnapshot = {
|
||
elements: state.elements.map(el => ({
|
||
path: el.path,
|
||
type: el.type,
|
||
x: parseFloat(el.element.dataset.x) || 0,
|
||
y: parseFloat(el.element.dataset.y) || 0,
|
||
width: el.width,
|
||
height: el.height,
|
||
loopPairs: el.loopPairs,
|
||
activeLoopPair: el.activeLoopPair
|
||
})),
|
||
currentScale: state.currentScale,
|
||
translate: { ...state.translate }
|
||
}
|
||
history.redoStack.push(currentSnapshot)
|
||
|
||
// 恢复上一个状态
|
||
const snapshot = history.undoStack.pop()
|
||
restoreStateFromSnapshot(snapshot)
|
||
|
||
console.log('Undo performed, undo stack:', history.undoStack.length, 'redo stack:', history.redoStack.length)
|
||
}
|
||
|
||
// 重做
|
||
function redo() {
|
||
if (history.redoStack.length === 0) {
|
||
console.log('Nothing to redo')
|
||
return
|
||
}
|
||
|
||
// 保存当前状态到撤销栈
|
||
const currentSnapshot = {
|
||
elements: state.elements.map(el => ({
|
||
path: el.path,
|
||
type: el.type,
|
||
x: parseFloat(el.element.dataset.x) || 0,
|
||
y: parseFloat(el.element.dataset.y) || 0,
|
||
width: el.width,
|
||
height: el.height,
|
||
loopPairs: el.loopPairs,
|
||
activeLoopPair: el.activeLoopPair
|
||
})),
|
||
currentScale: state.currentScale,
|
||
translate: { ...state.translate }
|
||
}
|
||
history.undoStack.push(currentSnapshot)
|
||
|
||
// 恢复重做状态
|
||
const snapshot = history.redoStack.pop()
|
||
restoreStateFromSnapshot(snapshot)
|
||
|
||
console.log('Redo performed, undo stack:', history.undoStack.length, 'redo stack:', history.redoStack.length)
|
||
}
|
||
|
||
// 从快照恢复状态
|
||
function restoreStateFromSnapshot(snapshot) {
|
||
// 清除当前所有元素
|
||
state.elements.forEach(el => el.element.remove())
|
||
state.elements = []
|
||
|
||
// 恢复缩放和平移
|
||
updateScaleAndTranslate(snapshot.currentScale, snapshot.translate)
|
||
|
||
// 恢复所有元素
|
||
snapshot.elements.forEach(elementData => {
|
||
addMediaWithPath(elementData.path, elementData.type, elementData)
|
||
})
|
||
}
|
||
|
||
let videoExample = {
|
||
loopPairs: [[0, 119], [44, 56]], // can derive A, B, C & color coding from index
|
||
activeLoopPair: 0
|
||
}
|
||
function closeEditVideo() {
|
||
//state.editVideo.videoElement.removeEventListener('timeupdate', onPlayerProgress)
|
||
state.mode = 'standard'
|
||
updateScaleAndTranslate(state.editVideo.backupState.currentScale, state.editVideo.backupState.translate)
|
||
state.editVideo.video.element.className = state.editVideo.backupState.elementClasses
|
||
document.querySelector('#root').classList.remove('disableBorder')
|
||
document.querySelector('#editVideoTools').classList.add('hide')
|
||
state.editVideo = {}
|
||
}
|
||
|
||
function editVideo(video) {
|
||
console.log('video', video)
|
||
state.mode = 'edit-video'
|
||
document.querySelector('#root').style.cursor = ""
|
||
state.editVideo.video = video
|
||
state.editVideo.backupState = {
|
||
currentScale: state.currentScale,
|
||
translate: Object.assign({}, state.translate),
|
||
elementClasses: video.element.className
|
||
}
|
||
updateScaleAndTranslate(1, {
|
||
translateX: 0,
|
||
translateY: 0
|
||
})
|
||
|
||
video.element.className = "editVideo"
|
||
document.querySelector('#root').classList.add('disableBorder')
|
||
document.querySelector('#editVideoTools').classList.remove('hide')
|
||
|
||
document.getElementById('eventTrigger').dataset.changesliders = JSON.stringify(video.loopPairs[video.activeLoopPair])
|
||
|
||
if (video.type == 'youtube') {
|
||
state.editVideo.videoElement = document.querySelector('.editVideo iframe').contentDocument.querySelector('video')
|
||
} else {
|
||
state.editVideo.videoElement = document.querySelector('.editVideo')
|
||
}
|
||
|
||
//state.editVideo.videoElement.addEventListener('timeupdate', onPlayerProgress)
|
||
//
|
||
|
||
console.log(video)
|
||
}
|
||
|
||
function onPlayerProgress(e) {
|
||
//am i editvideo?
|
||
// handle edit stuff
|
||
let sliderPositions = document.querySelectorAll('.noUi-handle')
|
||
let leftSliderPercent = Math.min(sliderPositions[0]['ariaValueText'], sliderPositions[1]['ariaValueText'])
|
||
let rightSliderPercent = Math.max(sliderPositions[0]['ariaValueText'], sliderPositions[1]['ariaValueText'])
|
||
//state.editVideo.video
|
||
let sliderElement = document.querySelector('#slider')
|
||
let currentTimePercent = (this.currentTime / this.duration * 100)
|
||
|
||
if (this != state.editVideo.videoElement) {
|
||
|
||
let leftPercent = parseFloat(this.dataset.loopLeft)
|
||
let rightPercent = parseFloat(this.dataset.loopRight)
|
||
//console.log('im not edit video',leftPercent, rightPercent)
|
||
if (leftPercent > currentTimePercent) {
|
||
this.currentTime = percentToCurrentTime(leftPercent, this.duration)
|
||
currentTimePercent = (this.currentTime / this.duration * 100)
|
||
}
|
||
if (rightPercent < currentTimePercent) {
|
||
this.currentTime = percentToCurrentTime(leftPercent, this.duration)
|
||
currentTimePercent = (this.currentTime / this.duration * 100)
|
||
}
|
||
return;
|
||
}
|
||
|
||
//console.log('im edit video')
|
||
|
||
state.editVideo.video.loopPairs[state.editVideo.video.activeLoopPair][0] = leftSliderPercent
|
||
state.editVideo.video.loopPairs[state.editVideo.video.activeLoopPair][1] = rightSliderPercent
|
||
this.dataset.loopLeft = leftSliderPercent
|
||
this.dataset.loopRight = rightSliderPercent
|
||
|
||
//console.log(state.editVideo.video.loopPairs, leftSliderPercent, rightSliderPercent)
|
||
|
||
if (sliderElement.classList.contains('noUi-state-drag')) {
|
||
dragPercent = parseFloat(sliderElement.querySelector('.noUi-active')['ariaValueNow'])
|
||
this.currentTime = percentToCurrentTime(dragPercent, this.duration)
|
||
} else {
|
||
if (leftSliderPercent > currentTimePercent) {
|
||
this.currentTime = percentToCurrentTime(leftSliderPercent, this.duration)
|
||
currentTimePercent = (this.currentTime / this.duration * 100)
|
||
}
|
||
if (rightSliderPercent < currentTimePercent) {
|
||
//.noUi-state-drag
|
||
if (sliderElement.classList.contains('noUi-state-drag')) {
|
||
this.currentTime = percentToCurrentTime(rightSliderPercent, this.duration)
|
||
} else
|
||
this.currentTime = percentToCurrentTime(leftSliderPercent, this.duration)
|
||
|
||
currentTimePercent = (this.currentTime / this.duration * 100)
|
||
}
|
||
}
|
||
let progressBarWidth = sliderElement.getBoundingClientRect().width // 10
|
||
|
||
let progressBarTimePosition = progressBarWidth * (currentTimePercent / 100) // 4
|
||
// transform: translateX(41.4966%);
|
||
document.getElementById('progressbar').style.transform = `translateX(${progressBarTimePosition}px)`
|
||
//sliderElement.style.background = `linear-gradient(90deg, rgba(78,47,102,1) 0%, rgba(91,52,122,1) ${progressBarTimePosition}px, rgba(113,80,136,1) ${progressBarTimePosition}px, rgba(121,76,157,1) 100%)`
|
||
|
||
}
|
||
function percentToCurrentTime(percent, duration) {
|
||
if (percent >= 100) return duration
|
||
if (percent <= 0) return 0
|
||
let ct = (percent * duration) / 100
|
||
console.log("percentToCurrentTime", ct)
|
||
return ct
|
||
}
|
||
|
||
function newScene() {
|
||
document.getElementById('itemHolder').innerHTML = ''
|
||
updateScaleAndTranslate(1, {
|
||
translateX: 0,
|
||
translateY: 0
|
||
});
|
||
initWorkspace()
|
||
refreshWorkspace()
|
||
|
||
state.elements = []
|
||
document.querySelector('#welcome').classList.remove("hide")
|
||
}
|
||
|
||
function loadState(loadedState, filePath) {
|
||
// file stuff
|
||
|
||
if (state.mode == 'init')
|
||
init();
|
||
|
||
|
||
newScene()
|
||
updateScaleAndTranslate(loadedState.currentScale, loadedState.translate)
|
||
|
||
setTimeout(function () {
|
||
for (var i in loadedState.elements) {
|
||
addMediaWithPath(loadedState.elements[i].path, loadedState.elements[i].type, loadedState.elements[i])
|
||
}
|
||
ipcRenderer.send('loaded-state', filePath)
|
||
}, 1000);
|
||
}
|
||
|
||
document.addEventListener('keydown', evt => {
|
||
if (state.isLocked) return
|
||
mouseObj.keys[evt.key] = true
|
||
|
||
if (evt.key === 'Escape') {
|
||
// 取消选择框
|
||
if (mouseObj.selecting) {
|
||
hideSelectionBox()
|
||
}
|
||
} else if (evt.key === 'h' && evt.ctrlKey) {
|
||
evt.preventDefault()
|
||
ipcRenderer.send('show-help')
|
||
console.log('Ctrl+H was pressed')
|
||
} else if (evt.key === 'c' && evt.ctrlKey) {
|
||
evt.preventDefault()
|
||
copySelected()
|
||
console.log('Ctrl+C was pressed')
|
||
} else if (evt.key === 'z' && evt.ctrlKey && !evt.shiftKey) {
|
||
evt.preventDefault()
|
||
undo()
|
||
console.log('Ctrl+Z was pressed (Undo)')
|
||
} else if (evt.key === 'y' && evt.ctrlKey) {
|
||
evt.preventDefault()
|
||
redo()
|
||
console.log('Ctrl+Y was pressed (Redo)')
|
||
} else if (evt.key === 'z' && evt.ctrlKey && evt.shiftKey) {
|
||
evt.preventDefault()
|
||
redo()
|
||
console.log('Ctrl+Shift+Z was pressed (Redo)')
|
||
} else if (evt.key === 'Delete') {
|
||
console.log('delete selected')
|
||
deleteSelected()
|
||
} else if (evt.key === 'v' && evt.ctrlKey) {
|
||
evt.preventDefault()
|
||
ipcRenderer.send('handle-paste')
|
||
console.log('Ctrl+V was pressed');
|
||
} else if (evt.key === 'l' && evt.ctrlKey) {
|
||
evt.preventDefault()
|
||
ipcRenderer.send('trigger-load')
|
||
console.log('Ctrl+L was pressed');
|
||
} else if (evt.key === 's' && evt.ctrlKey) {
|
||
evt.preventDefault()
|
||
ipcRenderer.send('trigger-save')
|
||
console.log('Ctrl+S was pressed');
|
||
} else if (evt.key === 'n' && evt.ctrlKey) {
|
||
evt.preventDefault()
|
||
newScene()
|
||
console.log('Ctrl+N was pressed');
|
||
} else if (evt.key === 'w' && evt.ctrlKey) {
|
||
evt.preventDefault()
|
||
ipcRenderer.send('trigger-close')
|
||
console.log('Ctrl+W was pressed');
|
||
} else if (evt.key === 'f' && evt.ctrlKey) {
|
||
evt.preventDefault()
|
||
ipcRenderer.send('trigger-maximize')
|
||
console.log('Ctrl+F was pressed');
|
||
} else if (evt.key === 'm' && evt.ctrlKey) {
|
||
evt.preventDefault()
|
||
ipcRenderer.send('trigger-minimize')
|
||
console.log('Ctrl+M was pressed');
|
||
} else if (evt.key === ' ' && evt.ctrlKey) {
|
||
mouseObj.ctrlSpace = true;
|
||
console.log('Ctrl+space was pressed');
|
||
} else if (evt.key === ' ') {
|
||
mouseObj.space = true;
|
||
}
|
||
});
|
||
|
||
document.addEventListener('keyup', evt => {
|
||
mouseObj.keys[evt.key] = false
|
||
console.log('keyup', evt.key);
|
||
if (evt.key === ' ' || evt.key == 'Control') {
|
||
mouseObj.ctrlSpace = false;
|
||
console.log('Ctrl+space was released');
|
||
if (evt.key === ' ') {
|
||
mouseObj.space = false;
|
||
}
|
||
}
|
||
|
||
})
|
||
|
||
function getSelected() {
|
||
if (document.querySelector('.editVideo')) return { type: 'edit-video' }
|
||
let lastIndex = state.elements.length - 1
|
||
if (!state.elements[lastIndex] || !state.elements[lastIndex].element.classList.contains('selectedItem')) return null
|
||
|
||
return state.elements[lastIndex]
|
||
}
|
||
document.addEventListener('contextmenu', (e) => {
|
||
e.preventDefault()
|
||
console.log(state)
|
||
if (mouseObj.dragging) {
|
||
mouseObj.dragging = false;
|
||
return;
|
||
}
|
||
|
||
|
||
})
|
||
let mouseObj = {
|
||
initPos: null,
|
||
initClientPos: null,
|
||
dragging: false,
|
||
ctrlSpace: false,
|
||
space: false,
|
||
keys: new Object(),
|
||
//time dragging & distance dragged
|
||
selecting: false,
|
||
selectionStart: null
|
||
}
|
||
document.addEventListener('mousedown', (e) => {
|
||
if (state.isLocked) return
|
||
mouseObj.initPos = { x: e.clientX, y: e.clientY }
|
||
mouseObj.initClientPos = { x: e.screenX, y: e.screenY }
|
||
mouseObj.initTranslate = { x: (parseFloat(document.body.dataset.translateX) || 0), y: (parseFloat(document.body.dataset.translateY) || 0) }
|
||
ipcRenderer.send('record-window-size', window.innerHeight, window.innerHeight)
|
||
mouseObj.dragging = false
|
||
|
||
// 左键点击空白区域开始选择(不按任何修饰键)
|
||
if (e.button === 0 && !mouseObj.keys[' '] && !mouseObj.keys['Control'] &&
|
||
(e.target.id === 'root' || e.target.id === 'workspaceBox' || e.target.id === 'itemHolder')) {
|
||
mouseObj.selecting = true
|
||
mouseObj.selectionStart = { x: e.clientX, y: e.clientY }
|
||
}
|
||
})
|
||
document.addEventListener('mousemove', (e) => {
|
||
if (state.isLocked) return
|
||
|
||
// 处理选择框拖动
|
||
if (mouseObj.selecting && e.buttons === 1) {
|
||
updateSelectionBox(e.clientX, e.clientY)
|
||
e.preventDefault()
|
||
return
|
||
}
|
||
|
||
// ctrl + space + mouse drag = zoom
|
||
if (e.buttons == 1 && mouseObj.keys[' '] && mouseObj.keys['Control']) {
|
||
//console.log(`handleZoom(${e.movementX})`);
|
||
handleZoom(e.movementX, mouseObj.initPos.x, mouseObj.initPos.y)
|
||
e.preventDefault()
|
||
} else if (e.buttons == 1 && mouseObj.space) {
|
||
e.preventDefault()
|
||
handleMove(e.movementX, e.movementY)
|
||
} else if (e.buttons == 4) { // middle mouse drag = move
|
||
e.preventDefault()
|
||
handleMove(e.movementX, e.movementY)
|
||
} else if (e.buttons == 2) {
|
||
e.preventDefault()
|
||
ipcRenderer.send('move-electron-window', e.screenX, e.screenY, mouseObj.initPos)
|
||
mouseObj.dragging = true;
|
||
|
||
}
|
||
|
||
})
|
||
function handleMove(dX, dY) {
|
||
currentScale = parseFloat(document.body.dataset.currentScale) || 1
|
||
|
||
|
||
let translateX = (parseFloat(document.body.dataset.translateX) || 0);
|
||
let translateY = (parseFloat(document.body.dataset.translateY) || 0);//(parseFloat(document.body.dataset.translateY) || 0);
|
||
translateX += dX
|
||
translateY += dY //- mouseObj.initTranslate.y
|
||
|
||
updateScaleAndTranslate(currentScale, { translateX, translateY })
|
||
}
|
||
|
||
function handleZoom(_delta, clientX, clientY) {
|
||
if (document.querySelector('.editVideo')) return;
|
||
currentScale = parseFloat(document.body.dataset.currentScale) || 1
|
||
let delta = _delta / 60
|
||
|
||
const nextScale = Math.max(currentScale + delta * (currentScale / 2), 0.01)
|
||
const ratio = 1 - nextScale / currentScale
|
||
let translateX = (parseFloat(document.body.dataset.translateX) || 0);
|
||
let translateY = (parseFloat(document.body.dataset.translateY) || 0);
|
||
|
||
translateX += (clientX - translateX) * ratio
|
||
translateY += (clientY - translateY) * ratio
|
||
|
||
currentScale = nextScale
|
||
updateScaleAndTranslate(currentScale, { translateX, translateY })
|
||
//zoom(nextScale, e)
|
||
}
|
||
document.addEventListener('mouseup', (e) => {
|
||
if (state.isLocked) return
|
||
|
||
// 处理选择框结束
|
||
if (mouseObj.selecting) {
|
||
finishSelection()
|
||
hideSelectionBox()
|
||
if (e.button === 0) {
|
||
return
|
||
}
|
||
}
|
||
|
||
console.log(e.button, mouseObj.dragging)
|
||
if (e.button == 2) {
|
||
if (mouseObj.dragging) {
|
||
|
||
distance = Math.sqrt(
|
||
Math.pow(e.screenX - mouseObj.initClientPos.x, 2)
|
||
+
|
||
Math.pow(e.screenY - mouseObj.initClientPos.y, 2));
|
||
console.log(distance)
|
||
if (distance < 4) {
|
||
|
||
ipcRenderer.send('show-context-menu', getSelected()?.type || "void")
|
||
}
|
||
mouseObj.dragging = false;
|
||
|
||
} else {
|
||
ipcRenderer.send('show-context-menu', getSelected()?.type || "void")
|
||
}
|
||
}
|
||
})
|
||
ipcRenderer.on('close-edit-video', (event, newState) => {
|
||
closeEditVideo()
|
||
})
|
||
ipcRenderer.on('edit-video', (event, newState) => {
|
||
editVideo(getSelected())
|
||
})
|
||
ipcRenderer.on('new-scene', (event) => {
|
||
newScene()
|
||
})
|
||
ipcRenderer.on('load-scene', (event, newState, filePath) => {
|
||
loadState(newState, filePath)
|
||
})
|
||
ipcRenderer.on('lock-window', (event) => {
|
||
state.isLocked = true
|
||
document.body.classList.add('locked')
|
||
clearAllSelected() // 取消所有选中的对象
|
||
})
|
||
ipcRenderer.on('unlock-window', (event) => {
|
||
state.isLocked = false
|
||
document.body.classList.remove('locked')
|
||
})
|
||
ipcRenderer.on('save-scene', (event, filePath) => {
|
||
var stateCopy = JSON.parse(JSON.stringify(state));
|
||
for (var i = 0; i < stateCopy.elements.length; i++) {
|
||
//stateCopy.elements.push()
|
||
delete stateCopy.elements[i].element;
|
||
}
|
||
console.log(stateCopy)
|
||
ipcRenderer.send('save-scene', filePath, stateCopy)
|
||
})
|
||
ipcRenderer.on('clipboard', (event, msg) => {
|
||
let payload = JSON.parse(msg);
|
||
console.log(payload)
|
||
if (/youtube.com\/.*v=([^\?]*)/.test(payload[payload.type])) {
|
||
addMediaWithPath(payload[payload.type], "youtube")
|
||
} else
|
||
addMediaWithPath(payload[payload.type], payload.type)
|
||
})
|
||
function getCenterOfWindowScaled() {
|
||
const width = window.innerWidth;
|
||
const height = window.innerHeight;
|
||
|
||
const widthScaled = (width / 2) / state.currentScale;
|
||
const heightScaled = (height / 2) / state.currentScale;
|
||
return {
|
||
centerX: (-state.translate.translateX / state.currentScale) + widthScaled,
|
||
centerY: (-state.translate.translateY / state.currentScale) + heightScaled
|
||
};
|
||
}
|
||
function addMediaWithPath(path, type = 'img', loadedState) {
|
||
isNewElement = loadedState == null
|
||
let centerWin = getCenterOfWindowScaled();
|
||
loadedState = loadedState || { x: centerWin.centerX, y: centerWin.centerY, width: null, height: null }
|
||
if (state.mode == 'init') init();
|
||
if (!document.querySelector('#welcome').classList.contains("hide"))
|
||
document.querySelector('#welcome').classList.add("hide");
|
||
let itemHolder = document.getElementById('itemHolder')
|
||
|
||
let mediaElement = undefined
|
||
if (type == 'img' || type == 'dataURL' || type == 'filePath') {
|
||
mediaElement = document.createElement('img')
|
||
if (isNewElement)
|
||
mediaElement.style.opacity = 0;
|
||
mediaElement.addEventListener('load', function loaded() {
|
||
let { x, y, width, height } = this.getClientRects()[0]
|
||
if (isNewElement) {
|
||
|
||
//loadedState.x = centerWin.centerX - width / 2
|
||
//loadedState.y = centerWin.centerY - height / 2
|
||
mediaElement.style.opacity = 1;
|
||
setTransformForElement(mediaElement.dataset.zIndex, -width / 2, -height / 2, width, height)
|
||
}
|
||
resizeWorkspaceToFitObj(loadedState.x, loadedState.y, width, height)
|
||
|
||
})
|
||
mediaElement.src = path;
|
||
} else if (type == 'video') {
|
||
mediaElement = document.createElement('video')
|
||
mediaElement.autoplay = true;
|
||
mediaElement.loop = true;
|
||
mediaElement.muted = true;
|
||
|
||
let srcElement = document.createElement('source')
|
||
srcElement.src = path;
|
||
mediaElement.appendChild(srcElement)
|
||
} else if (type == 'youtube') {
|
||
//mediaElement = document.createElement('iframe')
|
||
if (/youtube.com\/.*v=([^\?]*)/.test(path)) {
|
||
|
||
var code = extractYoutubeId(path)
|
||
if (code == null) return;
|
||
mediaElement = document.createElement('div')
|
||
mediaElement.classList.add('youtubePlayer')
|
||
mediaElement.classList.add('playerNeedsSetup')
|
||
mediaElement.dataset.idcode = code
|
||
|
||
mediaElement.style.width = (loadedState.width || 640) + "px";
|
||
mediaElement.style.height = (loadedState.height || 360) + "px";
|
||
|
||
//mediaElement.style.background = 'red'
|
||
var iframeDiv = document.createElement('div')
|
||
iframeDiv.classList.add('iframeDiv')
|
||
mediaElement.appendChild(iframeDiv)
|
||
document.getElementById('eventTrigger').dataset.youtubetrigger = Date.now()
|
||
/*
|
||
mediaElement.src =
|
||
`https://www.youtube.com/embed/${code}?` +//&autoplay=1` +
|
||
`&controls=0&disablekb=1&enablejsapi=1&fs=0&loop=1` +
|
||
`&origin=${window.location.href}`
|
||
mediaElement.frameBorder = "0"
|
||
mediaElement.allowFullscreen = false;
|
||
*/
|
||
}
|
||
} else if (type == "text") {
|
||
mediaElement = document.createElement('div')
|
||
mediaElement.classList.add('textElement')
|
||
mediaElement.innerText = path
|
||
|
||
console.log("isNewElement", isNewElement)
|
||
if (isNewElement) {
|
||
/*
|
||
mediaElement.style.fontSize = '1ch' // 1ch is the width of the 0 character
|
||
mediaElement.style.opacity = 0
|
||
document.body.appendChild(mediaElement)
|
||
let { x, y, width: widthOnDom, height: heightOnDom } = mediaElement.getClientRects()[0]
|
||
|
||
document.body.removeChild(mediaElement)
|
||
const windowWidth = window.innerWidth
|
||
|
||
const fontScaleFactor = windowWidth / widthOnDom
|
||
newWidth = windowWidth / state.currentScale
|
||
newHeight = (heightOnDom * (windowWidth / widthOnDom)) / state.currentScale
|
||
|
||
mediaElement.style.opacity = 1
|
||
mediaElement.style.fontSize = fontScaleFactor + "ch"
|
||
mediaElement.style.width = newWidth + "px";
|
||
mediaElement.style.height = newHeight + "px";
|
||
*/
|
||
|
||
const { width: newWidth, height: newHeight } = adjustFontSize2(mediaElement, path)
|
||
loadedState.x = centerWin.centerX - (newWidth / state.currentScale / 2)
|
||
loadedState.y = centerWin.centerY - (newHeight / state.currentScale / 2)
|
||
|
||
loadedState.width = newWidth
|
||
loadedState.height = newHeight
|
||
} else {
|
||
//debugger;
|
||
const { width: newWidth, height: newHeight } = adjustFontSize2(mediaElement, path, loadedState.width)
|
||
oldRatio = loadedState.width / loadedState.height
|
||
console.log("oldRatio", oldRatio, "newRatio", newWidth / newHeight)
|
||
mediaElement.style.width = loadedState.width + "px";
|
||
mediaElement.style.height = newHeight + "px";
|
||
loadedState.width = newWidth
|
||
loadedState.height = newHeight
|
||
}
|
||
mediaElement.width = loadedState.width
|
||
mediaElement.height = loadedState.height
|
||
|
||
} else {
|
||
alert('unsupported media type')
|
||
return;
|
||
}
|
||
mediaElement.classList.add('draggable')
|
||
|
||
zIndex = state.elements.length;
|
||
mediaElement.style.zIndex = zIndex
|
||
mediaElement.dataset.zIndex = zIndex
|
||
mediaElement.dataset.x = loadedState.x
|
||
mediaElement.dataset.y = loadedState.y
|
||
if (loadedState.width != null && loadedState.height != null) {
|
||
mediaElement.width = loadedState.width
|
||
mediaElement.height = loadedState.height
|
||
}
|
||
let mediaObj = {
|
||
path: path,
|
||
type: type,
|
||
element: mediaElement,
|
||
width: loadedState.width,
|
||
height: loadedState.height
|
||
}
|
||
if (type == 'youtube' || type == 'video') {
|
||
mediaObj.loopPairs = loadedState.loopPairs || [[0, 100]] // 0% & 100% positions for loop
|
||
mediaObj.activeLoopPair = loadedState.activeLoopPair || 0
|
||
}
|
||
|
||
state.elements.push(mediaObj)
|
||
if (type == 'video') {
|
||
mediaObj.element.dataset.loopLeft = mediaObj.loopPairs[mediaObj.activeLoopPair][0]
|
||
mediaObj.element.dataset.loopRight = mediaObj.loopPairs[mediaObj.activeLoopPair][1]
|
||
mediaObj.element.addEventListener('timeupdate', onPlayerProgress)
|
||
}
|
||
|
||
itemHolder.appendChild(mediaElement)
|
||
|
||
//debugger;
|
||
if (type == 'text') {
|
||
//setTransformForElement(zIndex, 0, 0, loadedState.width, loadedState.height)
|
||
console.log("loadedState", loadedState, mediaElement)
|
||
setTransformForElement(zIndex)
|
||
|
||
//adjustFontSize2(mediaElement, path, loadedState.width)
|
||
} else
|
||
setTransformForElement(zIndex)
|
||
|
||
// 保存历史记录(仅在添加新元素时,不在恢复状态时)
|
||
if (isNewElement) {
|
||
saveStateToHistory()
|
||
}
|
||
}
|
||
function adjustFontSize2(mediaElement, text, maxWidth = window.innerWidth) {
|
||
temp = document.createElement('div')
|
||
temp.style.fontSize = '1ch' // 1ch is the width of the 0 character
|
||
temp.style.position = 'absolute'
|
||
temp.style.opacity = 0
|
||
temp.style.color = "white"
|
||
temp.style.whiteSpace = "nowrap"
|
||
temp.innerText = text
|
||
document.getElementById('hiddenTextTester').appendChild(temp)
|
||
let { x, y, width: widthOnDom, height: heightOnDom } = temp.getClientRects()[0]
|
||
|
||
newRatio = widthOnDom / heightOnDom
|
||
console.log("widthOnDom", widthOnDom, "HeightOnDom", heightOnDom, temp.clientWidth, temp.clientHeight, newRatio)
|
||
document.getElementById('hiddenTextTester').removeChild(temp)
|
||
const windowWidth = maxWidth
|
||
|
||
const fontScaleFactor = (maxWidth / widthOnDom) //* state.currentScale
|
||
//const fontScaleFactorScaled = fontScaleFactor / state.currentScale
|
||
newWidth = maxWidth
|
||
//newWidthScaled = newWidth / state.currentScale
|
||
newHeight = (heightOnDom * fontScaleFactor)
|
||
//newHeightScaled = newHeight / state.currentScale
|
||
console.log("Adjustfont2", fontScaleFactor, state.currentScale)
|
||
mediaElement.style.opacity = 1
|
||
mediaElement.style.fontSize = fontScaleFactor * state.currentScale + "ch"
|
||
mediaElement.style.width = newWidth + "px";
|
||
mediaElement.style.height = newHeight + "px";
|
||
|
||
return { width: newWidth, height: newHeight }
|
||
}
|
||
|
||
document.addEventListener('drop', (event) => {
|
||
if (state.isLocked) return
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
// Todo check if file is valid
|
||
|
||
for (const f of event.dataTransfer.files) {
|
||
// Using the path attribute to get absolute file path
|
||
console.log('File Path of dragged files: ', f.path, state)
|
||
|
||
if (f.path.endsWith('.mp4')) {
|
||
addMediaWithPath(f.path, 'video')
|
||
} else {
|
||
// 发送文件路径到主进程,让主进程转换为 base64
|
||
ipcRenderer.send('convert-file-to-dataurl', f.path)
|
||
}
|
||
}
|
||
});
|
||
|
||
// 接收主进程转换后的 dataURL
|
||
ipcRenderer.on('file-converted-to-dataurl', (event, dataURL) => {
|
||
console.log('Received dataURL from main process')
|
||
addMediaWithPath(dataURL, 'dataURL')
|
||
});
|
||
function init() {
|
||
document.documentElement.addEventListener('mousedown', (event) => {
|
||
var target = event.target
|
||
//console.log('click', target)
|
||
|
||
if (target.id == 'root' || target.id == 'workspaceBox') {
|
||
console.log('background click')
|
||
clearAllSelected()
|
||
}
|
||
})
|
||
state.mode = 'standard'
|
||
}
|
||
|
||
document.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
});
|
||
function extractYoutubeId(path) {
|
||
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
|
||
var capture = path.match(regExp)//path.match(/v=([^\?]*)/);
|
||
|
||
return capture[7]
|
||
}
|
||
|
||
function clampWorkspaceTranslate(ratio, newTranslate) {
|
||
let { translateX, translateY } = newTranslate
|
||
|
||
let windowElement = document.getElementById('root')
|
||
let windowWidth = windowElement.clientWidth
|
||
let windowHeight = windowElement.clientHeight
|
||
|
||
if (-(translateX) > state.workspaceRect.x2 * ratio) { //workspace off screen to left
|
||
console.log('workspace off screen to left')
|
||
translateX = -(state.workspaceRect.x2 * ratio)
|
||
}
|
||
else if ((translateX) + (state.workspaceRect.x1 * ratio) > windowWidth) { // workspace off screen to right
|
||
console.log('workspace off screen to right')
|
||
translateX = windowWidth - (state.workspaceRect.x1 * ratio)
|
||
}
|
||
|
||
if (-(translateY) > state.workspaceRect.y2 * ratio) { // workspace off screen to top
|
||
console.log('workspace off screen to top')
|
||
translateY = -(state.workspaceRect.y2 * ratio)
|
||
}
|
||
else if ((translateY) + (state.workspaceRect.y1 * ratio) > windowHeight) { // workspace off screen to bottom
|
||
console.log('workspace off screen to bottom')
|
||
translateY = windowHeight - (state.workspaceRect.y1 * ratio)
|
||
}
|
||
return { translateX: translateX, translateY: translateY }
|
||
}
|
||
|
||
function updateScaleAndTranslate(newScale, newTranslate) {
|
||
if (newScale > 2) {
|
||
state.currentScale = 2
|
||
return;
|
||
}
|
||
|
||
newTranslate = clampWorkspaceTranslate(newScale, newTranslate)
|
||
state.currentScale = newScale
|
||
state.translate = newTranslate
|
||
document.body.style.transform = `translate(${state.translate.translateX}px, ${state.translate.translateY}px) scale(${state.currentScale})`
|
||
document.body.dataset.currentScale = state.currentScale
|
||
document.body.dataset.translateX = state.translate.translateX
|
||
document.body.dataset.translateY = state.translate.translateY
|
||
const ROOTCSS = document.querySelector(':root');
|
||
ROOTCSS.style.setProperty('--scale', newScale);
|
||
//window.currentScale = state.currentScale;
|
||
}
|
||
|
||
let objPlayground = { hi: 'yo' };
|
||
contextBridge.exposeInMainWorld('myAPI', {
|
||
updateScaleAndTranslate: updateScaleAndTranslate,
|
||
updateYoutubeOriginalSize: (idcode, w, h) => {
|
||
for (var i in state.elements) {
|
||
if (state.elements[i].type == 'youtube' && extractYoutubeId(state.elements[i].path) == idcode) {
|
||
|
||
state.elements[i].width = state.elements[i].width || w;
|
||
state.elements[i].height = state.elements[i].height || h;
|
||
|
||
state.elements[i].element.style.width = state.elements[i].width
|
||
state.elements[i].element.style.height = state.elements[i].height
|
||
|
||
let videoElement = state.elements[i].element.querySelector('iframe').contentDocument.querySelector('video');
|
||
videoElement.dataset.loopLeft = state.elements[i].loopPairs[state.elements[i].activeLoopPair][0]
|
||
videoElement.dataset.loopRight = state.elements[i].loopPairs[state.elements[i].activeLoopPair][1]
|
||
videoElement.addEventListener('timeupdate', onPlayerProgress)
|
||
console.log(state.elements[i])
|
||
}
|
||
}
|
||
|
||
},
|
||
objPlayground: objPlayground
|
||
|
||
})
|
||
|
||
interact('.draggable')
|
||
.draggable({
|
||
listeners: {
|
||
move: dragMoveListener,
|
||
end: function(event) {
|
||
// 拖动结束时保存历史记录
|
||
saveStateToHistory()
|
||
}
|
||
},
|
||
inertia: false,
|
||
}).on('tap', function (event) {
|
||
var target = event.target
|
||
|
||
handleSelected(target)
|
||
//
|
||
event.preventDefault()
|
||
})
|
||
function isMouseInBlockingState() {
|
||
if (mouseObj.keys[' '] && mouseObj.keys['Control']) {
|
||
return true
|
||
} else if (mouseObj.keys[' ']) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
interact('.selectedItem').resizable({
|
||
// resize from all edges and corners
|
||
//allowFrom: '.selectedItem',
|
||
edges: { left: true, right: true, bottom: true, top: true },
|
||
ratio: 1,
|
||
enabled: true,
|
||
margin: 4,
|
||
listeners: [{
|
||
move(event) {
|
||
if (isMouseInBlockingState()) return;
|
||
var target = event.target
|
||
//handleSelected(target, true)
|
||
console.log('resize')
|
||
|
||
if (target.classList.contains('textElement')) {
|
||
adjustFontSize2(target, target.innerText, event.rect.width)
|
||
}
|
||
setTransformForElement(target.dataset.zIndex, event.deltaRect.left, event.deltaRect.top, event.rect.width, event.rect.height)
|
||
forceRedraw()
|
||
},
|
||
end(event) {
|
||
// 调整大小结束时保存历史记录
|
||
saveStateToHistory()
|
||
}
|
||
}],
|
||
modifiers: [
|
||
interact.modifiers.aspectRatio({
|
||
// make sure the width is always double the height
|
||
ratio: 'preserve',
|
||
// also restrict the size by nesting another modifier
|
||
}),
|
||
// minimum size
|
||
interact.modifiers.restrictSize({
|
||
min: { width: 10 }
|
||
})
|
||
],
|
||
inertia: true
|
||
}).draggable({
|
||
listeners: {
|
||
move: dragMoveListener,
|
||
end: function(event) {
|
||
// 拖动结束时保存历史记录
|
||
saveStateToHistory()
|
||
}
|
||
},
|
||
inertia: false
|
||
}).on('tap', function (event) {
|
||
|
||
var target = event.target
|
||
handleSelected(target)
|
||
//
|
||
event.preventDefault()
|
||
})
|
||
|
||
function copySelected() {
|
||
if (state.elements.length == 0) return
|
||
|
||
// 获取所有选中的元素
|
||
const selectedElements = state.elements.filter(el =>
|
||
el.element.classList.contains('selectedItem')
|
||
)
|
||
|
||
if (selectedElements.length === 0) return
|
||
|
||
console.log('Copying', selectedElements.length, 'selected element(s)')
|
||
|
||
// 如果只选中一个元素
|
||
if (selectedElements.length === 1) {
|
||
const selectedElement = selectedElements[0]
|
||
|
||
if (selectedElement.type === 'text') {
|
||
// 文本元素:复制文本内容
|
||
ipcRenderer.send('copy-text', selectedElement.element.innerText)
|
||
console.log('Copying text:', selectedElement.element.innerText)
|
||
} else if (selectedElement.type === 'img' || selectedElement.type === 'dataURL' || selectedElement.type === 'filePath') {
|
||
// 图片元素:使用 Canvas 转换为高质量 PNG dataURL
|
||
copyImageToClipboard(selectedElement)
|
||
} else if (selectedElement.type === 'video' || selectedElement.type === 'youtube') {
|
||
// 视频元素:复制路径或链接
|
||
ipcRenderer.send('copy-text', selectedElement.path)
|
||
console.log('Copying video path:', selectedElement.path)
|
||
}
|
||
} else {
|
||
// 多个元素:将所有图片合并到一个画布上
|
||
copyMultipleImages(selectedElements)
|
||
}
|
||
}
|
||
|
||
function copyImageToClipboard(selectedElement) {
|
||
try {
|
||
const img = selectedElement.element
|
||
const imgSrc = img.src || selectedElement.path
|
||
|
||
console.log('Copying image - type:', selectedElement.type)
|
||
console.log('Image src:', imgSrc.substring(0, 100))
|
||
|
||
// 检查是否是 GIF(通过 dataURL 或文件扩展名)
|
||
const isGif = imgSrc.startsWith('data:image/gif') ||
|
||
imgSrc.toLowerCase().endsWith('.gif') ||
|
||
(selectedElement.path && selectedElement.path.toLowerCase().endsWith('.gif'))
|
||
|
||
if (isGif && imgSrc.startsWith('data:')) {
|
||
// 如果是 GIF 的 dataURL,直接使用原始数据
|
||
console.log('Copying GIF with original data (preserving animation)')
|
||
ipcRenderer.send('copy-image', imgSrc)
|
||
} else if (selectedElement.type === 'dataURL' && imgSrc.startsWith('data:')) {
|
||
// 如果是其他格式的 dataURL,也直接使用
|
||
console.log('Copying image with original dataURL')
|
||
ipcRenderer.send('copy-image', imgSrc)
|
||
} else {
|
||
// 对于非 dataURL 的图片,使用 Canvas 转换
|
||
const canvas = document.createElement('canvas')
|
||
canvas.width = img.naturalWidth || img.width
|
||
canvas.height = img.naturalHeight || img.height
|
||
|
||
const ctx = canvas.getContext('2d')
|
||
ctx.drawImage(img, 0, 0)
|
||
|
||
// 转换为 PNG 格式的 dataURL
|
||
const pngDataURL = canvas.toDataURL('image/png')
|
||
|
||
console.log('Copying image converted to PNG, size:', canvas.width, 'x', canvas.height)
|
||
ipcRenderer.send('copy-image', pngDataURL)
|
||
}
|
||
} catch (err) {
|
||
console.error('Error copying image:', err)
|
||
// 降级方案:直接使用原始路径
|
||
const imgSrc = selectedElement.element.src || selectedElement.path
|
||
ipcRenderer.send('copy-image', imgSrc)
|
||
}
|
||
}
|
||
|
||
async function copyMultipleImages(selectedElements) {
|
||
try {
|
||
// 过滤出图片元素
|
||
const imageElements = selectedElements.filter(el =>
|
||
el.type === 'img' || el.type === 'dataURL' || el.type === 'filePath'
|
||
)
|
||
|
||
if (imageElements.length === 0) {
|
||
console.log('No images to copy')
|
||
return
|
||
}
|
||
|
||
console.log('Copying', imageElements.length, 'images')
|
||
|
||
// 收集所有图片的原始数据(保持 GIF 动画)
|
||
const imageDataURLs = []
|
||
|
||
for (const el of imageElements) {
|
||
try {
|
||
const img = el.element
|
||
const imgSrc = img.src || el.path
|
||
|
||
// 检查是否是 GIF
|
||
const isGif = imgSrc.startsWith('data:image/gif') ||
|
||
imgSrc.toLowerCase().endsWith('.gif') ||
|
||
(el.path && el.path.toLowerCase().endsWith('.gif'))
|
||
|
||
if (isGif && imgSrc.startsWith('data:')) {
|
||
// GIF dataURL - 直接使用原始数据
|
||
console.log(' Preserving GIF animation for element')
|
||
imageDataURLs.push(imgSrc)
|
||
} else if (el.type === 'dataURL' && imgSrc.startsWith('data:')) {
|
||
// 其他 dataURL - 直接使用
|
||
console.log(' Using original dataURL for element')
|
||
imageDataURLs.push(imgSrc)
|
||
} else {
|
||
// 其他格式 - 转换为 PNG
|
||
const canvas = document.createElement('canvas')
|
||
canvas.width = img.naturalWidth || img.width
|
||
canvas.height = img.naturalHeight || img.height
|
||
|
||
const ctx = canvas.getContext('2d')
|
||
ctx.drawImage(img, 0, 0)
|
||
|
||
const pngDataURL = canvas.toDataURL('image/png')
|
||
console.log(' Converted to PNG for element')
|
||
imageDataURLs.push(pngDataURL)
|
||
}
|
||
} catch (err) {
|
||
console.error('Error processing image:', err)
|
||
}
|
||
}
|
||
|
||
if (imageDataURLs.length === 0) {
|
||
console.log('No valid images to copy')
|
||
return
|
||
}
|
||
|
||
// 发送到主进程处理
|
||
console.log('Sending', imageDataURLs.length, 'images to main process')
|
||
ipcRenderer.send('copy-multiple-images', imageDataURLs)
|
||
|
||
} catch (err) {
|
||
console.error('Error copying multiple images:', err)
|
||
ipcRenderer.send('copy-text', '复制多张图片失败: ' + err.message)
|
||
}
|
||
}
|
||
|
||
function deleteSelected() {
|
||
if (state.elements.length == 0) return
|
||
|
||
// 收集所有选中的元素
|
||
const selectedElements = state.elements.filter(el =>
|
||
el.element.classList.contains('selectedItem')
|
||
)
|
||
|
||
if (selectedElements.length === 0) return
|
||
|
||
console.log('Deleting', selectedElements.length, 'selected element(s)')
|
||
|
||
// 保存历史记录
|
||
saveStateToHistory()
|
||
|
||
// 删除所有选中的元素
|
||
selectedElements.forEach(elementObj => {
|
||
// 从 DOM 中移除
|
||
elementObj.element.remove()
|
||
|
||
// 从 state.elements 数组中移除
|
||
const index = state.elements.indexOf(elementObj)
|
||
if (index > -1) {
|
||
state.elements.splice(index, 1)
|
||
}
|
||
})
|
||
|
||
// 重新设置所有剩余元素的 zIndex
|
||
state.elements.forEach((elementObj, index) => {
|
||
elementObj.element.style.zIndex = index
|
||
elementObj.element.dataset.zIndex = index
|
||
})
|
||
|
||
clearAllSelected()
|
||
}
|
||
|
||
function clearAllSelected() {
|
||
document.querySelectorAll('.selectedItem').forEach((elm) => {
|
||
elm.classList.remove('selectedItem')
|
||
elm.classList.add('draggable')
|
||
})
|
||
}
|
||
|
||
function handleSelected(target, dragging = false) {
|
||
if (isMouseInBlockingState()) return;
|
||
|
||
clearAllSelected()
|
||
target.classList.remove('draggable')
|
||
target.classList.add('selectedItem')
|
||
|
||
if (state.elements.length > 1) {
|
||
let targetIndex = parseInt(target.dataset.zIndex);
|
||
|
||
state.elements.push(state.elements.splice(targetIndex, 1)[0]);
|
||
for (var i = targetIndex; i < state.elements.length; i++) { // i can start at targetIndex
|
||
state.elements[i].element.style.zIndex = i;
|
||
state.elements[i].element.dataset.zIndex = i;
|
||
}
|
||
}
|
||
}
|
||
|
||
function dragMoveListener(event) {
|
||
if (isMouseInBlockingState()) return;
|
||
var target = event.target
|
||
handleSelected(target, true)
|
||
setTransformForElement(target.dataset.zIndex, event.dx, event.dy)
|
||
forceRedraw()
|
||
}
|
||
|
||
function resizeWorkspaceToFitObj(x, y, width, height) {
|
||
if (width == 0 || height == 0) return
|
||
state.workspaceRect.x1 = Math.min(x, state.workspaceRect.x1)
|
||
state.workspaceRect.y1 = Math.min(y, state.workspaceRect.y1)
|
||
|
||
state.workspaceRect.x2 = Math.max(x + width, state.workspaceRect.x2)
|
||
state.workspaceRect.y2 = Math.max(y + height, state.workspaceRect.y2)
|
||
refreshWorkspace()
|
||
}
|
||
|
||
function initWorkspace() {
|
||
state.workspaceRect = {
|
||
x1: 0,
|
||
y1: 0,
|
||
x2: 0,
|
||
y2: 0
|
||
}
|
||
}
|
||
|
||
function refreshWorkspace() {
|
||
const element = document.getElementById('workspaceBox');
|
||
element.style.left = state.workspaceRect.x1 + "px";
|
||
element.style.top = state.workspaceRect.y1 + "px";
|
||
element.style.width = (state.workspaceRect.x2 - state.workspaceRect.x1) + "px"
|
||
element.style.height = (state.workspaceRect.y2 - state.workspaceRect.y1) + "px"
|
||
|
||
}
|
||
|
||
function setTransformForElement(elementIndex, dx = 0, dy = 0, width = null, height = null) {
|
||
let elementObj = state.elements[elementIndex]
|
||
let x = (parseFloat(elementObj.element.dataset.x) || 0) + (dx / state.currentScale)
|
||
let y = (parseFloat(elementObj.element.dataset.y) || 0) + (dy / state.currentScale)
|
||
|
||
elementObj.element.setAttribute('data-x', x)
|
||
elementObj.element.setAttribute('data-y', y)
|
||
elementObj.x = x
|
||
elementObj.y = y
|
||
|
||
if (width != null && height != null) {
|
||
elementObj.width = width / state.currentScale
|
||
elementObj.height = height / state.currentScale
|
||
elementObj.element.style.width = width / state.currentScale + 'px'
|
||
elementObj.element.style.height = height / state.currentScale + 'px'
|
||
}
|
||
|
||
resizeWorkspaceToFitObj(x, y, elementObj.width || elementObj.element.width, elementObj.height || elementObj.element.height)
|
||
|
||
elementObj.element.style.transform = 'translate(' + x + 'px, ' + y + 'px)'
|
||
}
|
||
|
||
function forceRedraw() {
|
||
if (document.body.parentElement.style.backgroundColor == '') {
|
||
document.body.parentElement.style.backgroundColor = '#04040400'
|
||
} else {
|
||
document.body.parentElement.style.backgroundColor = ''
|
||
}
|
||
}
|
||
|
||
// 选择框相关函数
|
||
function hideSelectionBox() {
|
||
const selectionBox = document.getElementById('selectionBox')
|
||
if (selectionBox) {
|
||
selectionBox.style.display = 'none'
|
||
selectionBox.style.width = '0px'
|
||
selectionBox.style.height = '0px'
|
||
}
|
||
mouseObj.selecting = false
|
||
mouseObj.selectionStart = null
|
||
mouseObj.selectionWorldRect = null
|
||
}
|
||
|
||
function updateSelectionBox(currentX, currentY) {
|
||
const selectionBox = document.getElementById('selectionBox')
|
||
if (!selectionBox || !mouseObj.selectionStart) return
|
||
|
||
// 将屏幕坐标转换为世界坐标(body 内的坐标系统)
|
||
const startWorldX = (mouseObj.selectionStart.x - state.translate.translateX) / state.currentScale
|
||
const startWorldY = (mouseObj.selectionStart.y - state.translate.translateY) / state.currentScale
|
||
const currentWorldX = (currentX - state.translate.translateX) / state.currentScale
|
||
const currentWorldY = (currentY - state.translate.translateY) / state.currentScale
|
||
|
||
const worldLeft = Math.min(startWorldX, currentWorldX)
|
||
const worldTop = Math.min(startWorldY, currentWorldY)
|
||
const worldWidth = Math.abs(currentWorldX - startWorldX)
|
||
const worldHeight = Math.abs(currentWorldY - startWorldY)
|
||
|
||
// 直接使用世界坐标,因为选择框在 body 内,会自动应用 body 的 transform
|
||
selectionBox.style.left = worldLeft + 'px'
|
||
selectionBox.style.top = worldTop + 'px'
|
||
selectionBox.style.width = worldWidth + 'px'
|
||
selectionBox.style.height = worldHeight + 'px'
|
||
selectionBox.style.display = 'block'
|
||
|
||
// 保存世界坐标用于后续检测
|
||
mouseObj.selectionWorldRect = {
|
||
left: worldLeft,
|
||
top: worldTop,
|
||
right: worldLeft + worldWidth,
|
||
bottom: worldTop + worldHeight
|
||
}
|
||
}
|
||
|
||
function finishSelection() {
|
||
const selectionBox = document.getElementById('selectionBox')
|
||
if (!selectionBox || !mouseObj.selectionWorldRect) return
|
||
|
||
// 清除之前的选择
|
||
clearAllSelected()
|
||
|
||
// 检查每个元素是否与选择框相交(使用世界坐标)
|
||
let selectedElements = []
|
||
state.elements.forEach((elementObj, index) => {
|
||
// 获取元素的世界坐标
|
||
const elemX = parseFloat(elementObj.element.dataset.x) || 0
|
||
const elemY = parseFloat(elementObj.element.dataset.y) || 0
|
||
const elemWidth = elementObj.width || elementObj.element.width || 0
|
||
const elemHeight = elementObj.height || elementObj.element.height || 0
|
||
|
||
const elementWorldRect = {
|
||
left: elemX,
|
||
top: elemY,
|
||
right: elemX + elemWidth,
|
||
bottom: elemY + elemHeight
|
||
}
|
||
|
||
// 检查矩形是否相交
|
||
if (isRectIntersecting(mouseObj.selectionWorldRect, elementWorldRect)) {
|
||
selectedElements.push(index)
|
||
}
|
||
})
|
||
|
||
// 选中所有相交的元素
|
||
if (selectedElements.length > 0) {
|
||
// 将最后一个选中的元素移到最上层
|
||
const lastIndex = selectedElements[selectedElements.length - 1]
|
||
handleSelected(state.elements[lastIndex].element)
|
||
|
||
// 标记其他选中的元素
|
||
selectedElements.slice(0, -1).forEach(index => {
|
||
state.elements[index].element.classList.add('selectedItem')
|
||
state.elements[index].element.classList.remove('draggable')
|
||
})
|
||
}
|
||
|
||
}
|
||
|
||
function isRectIntersecting(rect1, rect2) {
|
||
return !(rect1.right < rect2.left ||
|
||
rect1.left > rect2.right ||
|
||
rect1.bottom < rect2.top ||
|
||
rect1.top > rect2.bottom)
|
||
}
|
||
|
||
|
||
window.addEventListener('load', (event) => {
|
||
setTimeout(function () {
|
||
console.log('page is fully loaded');
|
||
ipcRenderer.send('ready')
|
||
}, 2000);
|
||
|
||
});
|
||
|
||
// 窗口获得焦点时重置键盘状态
|
||
window.addEventListener('focus', () => {
|
||
console.log('Window focused - resetting keyboard state')
|
||
mouseObj.keys = new Object()
|
||
mouseObj.ctrlSpace = false
|
||
mouseObj.space = false
|
||
// 如果有未完成的选择,清理它
|
||
if (mouseObj.selecting) {
|
||
hideSelectionBox()
|
||
}
|
||
});
|
||
|
||
// 窗口失去焦点时重置键盘状态
|
||
window.addEventListener('blur', () => {
|
||
console.log('Window blurred - resetting keyboard state')
|
||
mouseObj.keys = new Object()
|
||
mouseObj.ctrlSpace = false
|
||
mouseObj.space = false
|
||
// 如果有未完成的选择,清理它
|
||
if (mouseObj.selecting) {
|
||
hideSelectionBox()
|
||
}
|
||
});
|