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() } });