Files
pureRef/preload.js
小煜 73aaab6d36 feat: 初始化RefVroid项目,添加核心功能与资源文件
- 添加主窗口、帮助窗口和系统托盘功能
- 实现图片、视频和文本的拖放与粘贴功能
- 添加场景保存与加载功能
- 包含基本UI样式和快捷键帮助文档
- 添加.gitattributes和.npmrc配置文件
- 包含LICENSE许可文件和图标资源
2025-12-17 21:00:07 +08:00

1184 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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 === '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)
}
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
addMediaWithPath(f.path)
}
});
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 },
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()
}
}],
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 },
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 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 - type:', selectedElement.type, 'size:', canvas.width, 'x', canvas.height)
ipcRenderer.send('copy-image', pngDataURL)
} catch (err) {
console.error('Error converting image to canvas:', 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')
// 收集所有图片的 Blob
const imageBlobs = []
for (const el of imageElements) {
try {
const img = el.element
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)
// 转换为 Blob
const blob = await new Promise(resolve => {
canvas.toBlob(resolve, 'image/png')
})
if (blob) {
imageBlobs.push(blob)
}
} catch (err) {
console.error('Error converting image:', err)
}
}
if (imageBlobs.length === 0) {
console.log('No valid images to copy')
return
}
// 使用 Clipboard API 复制多张图片
try {
const clipboardItems = imageBlobs.map(blob =>
new ClipboardItem({ 'image/png': blob })
)
// 注意:标准 Clipboard API 一次只能写入一个 ClipboardItem
// 所以我们需要将多张图片合并或者只复制第一张
// 这里我们尝试写入所有图片,但浏览器可能只保留最后一张
for (const item of clipboardItems) {
await navigator.clipboard.write([item])
}
console.log('Copied', imageBlobs.length, 'images using Clipboard API')
// 降级方案:发送到主进程
const imageDataURLs = []
for (const el of imageElements) {
const img = el.element
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)
imageDataURLs.push(canvas.toDataURL('image/png'))
}
ipcRenderer.send('copy-multiple-images', imageDataURLs)
} catch (err) {
console.error('Clipboard API failed, using fallback:', err)
// 降级方案:发送到主进程
const imageDataURLs = []
for (const el of imageElements) {
const img = el.element
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)
imageDataURLs.push(canvas.toDataURL('image/png'))
}
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
if (!state.elements[state.elements.length - 1].element.classList.contains('selectedItem')) return
state.elements[state.elements.length - 1].element.remove();
//delete state.elements[state.elements.length - 1].element
state.elements.pop();
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()
}
});