This commit is contained in:
Askill 2020-04-30 20:27:28 +02:00
parent 0e6229834a
commit 670235b501
37 changed files with 25532 additions and 1 deletions

View File

@ -1 +1,14 @@
Photo-Wall # Photo-Wall
This project was started to provide an easy to use and free mockup tool for photographers.
You can use this tool to show photographs in different frames either at your own home or at a clients.
This includes the possibility of showing the exact dimensions of the picture in the chosen space.
#### Demo: [jpmatz.de](https://www.jpmatz.de/demo)
#### Desktop-App: [jpmatz.de](https://www.jpmatz.de)
![](./mock-it.png)
### Technical aspekt
To accomplish this goal a custom purely JS based render engine was created.
Since the entire application lives in the front-end a static webspace like a AWS-Bucket could be used for hosting, if one wanted to.

BIN
mock-it.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
web/backgrounds/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

BIN
web/backgrounds/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
web/backgrounds/2.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
web/backgrounds/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

BIN
web/backgrounds/3.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
web/backgrounds/4.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
web/backgrounds/5.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -0,0 +1 @@
["1.jpg", "2.jpeg", "3.jpeg", "4.jpeg", "5.jpeg"]

196
web/canvas.js Normal file
View File

@ -0,0 +1,196 @@
var backgroundPath = "";
var backImg = new Image();
var framePaths = [];
backImg.src = backgroundPath;
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var canvasOffset = $("#canvas").offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
var canvasWidth = canvas.width;
var canvasHeight = canvas.height;
var isDragging = false;
var isScaling = false;
var pictures = []; // the array with picture objects
var focusedID = 0; // id of focused object
var zoomLevel = 1;
var canMouseX;
var canMouseY;
var scale = 1;
var scaleRefPoints = [];
var pixelPerMeter = 0;
var scaleRefLenght = 1;
var showMeasurements = false;
// render "engine"
function draw() {
prepareCanvas()
// draw all picture objects
pictures.forEach(function (item) {
try {
item.calcPassp();
drawPassp(item);
drawImage(item);
drawFrame(item);
drawMeasurements(item)
}
catch (error) {
console.log(error);
}
});
drawRuler();
//residual from preparecanvas function
ctx.restore();
}
function drawX(x, y) {
ctx.moveTo(x - 5, y - 5);
ctx.lineTo(x + 5, y + 5);
ctx.lineWidth = 2;
ctx.stroke();
ctx.moveTo(x + 5, y - 5);
ctx.lineTo(x - 5, y + 5);
ctx.stroke();
}
function prepareCanvas() {
// scales the image to fit on the canvas
scale = canvasWidth / backImg.width;
if (backImg.height > backImg.width) {
scale = canvasHeight / backImg.height;
}
// offset for background to be centered
let offsetx = (canvasWidth - backImg.width * scale) / 2;
let offsety = (canvasHeight - backImg.height * scale) / 2;
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.save();
ctx.translate(canMouseX, canMouseY);
ctx.scale(zoomLevel, zoomLevel);
ctx.translate(-(canMouseX), -(canMouseY));
ctx.drawImage(backImg, offsetx, offsety, backImg.width * scale, backImg.height * scale);
}
function drawPassp(item) {
// draw passpatous
let img = item.img;
ctx.fillStyle = item.passpColor;
if (item.frameRotate) {
ctx.save();
ctx.translate(item.x + item.passpImg.width * item.passpScale / 2, item.y + item.passpImg.height * item.passpScale / 2); // change origin
ctx.rotate(Math.PI / 2);
ctx.translate(-(item.x + item.passpImg.width * item.passpScale / 2), -(item.y + item.passpImg.height * item.passpScale / 2)); // change origin
ctx.fillRect(
item.x - (item.passpImg.height * item.passpScale - (img.height * item.scale)) / 2 + item.passpOffset,
item.y + (item.passpImg.width * item.passpScale - (img.width * item.scale)) / 2 + item.passpOffset,
(item.passpImg.width * item.passpScale) - item.passpOffset * 2,
(item.passpImg.height * item.passpScale) - item.passpOffset * 2);
ctx.rotate(-Math.PI / 2);
ctx.restore()
}
else {
ctx.fillRect(
item.x - (item.passpImg.width * item.passpScale - (img.width * item.scale)) / 2 + item.passpOffset,
item.y - (item.passpImg.height * item.passpScale - (img.height * item.scale)) / 2 + item.passpOffset,
(item.passpImg.width * item.passpScale) - item.passpOffset * 2,
(item.passpImg.height * item.passpScale) - item.passpOffset * 2);
}
}
function drawImage(item) {
// draw image
let img = item.img;
ctx.save();
ctx.translate(item.x + item.img.width * item.scale / 2, item.y + item.img.height * item.scale / 2); // change origin
ctx.rotate(item.imgRotate);
ctx.translate(-(item.x + item.img.width * item.scale / 2), -(item.y + item.img.height * item.scale / 2)); // change origin
ctx.drawImage(img, item.x, item.y, img.width * item.scale, img.height * item.scale);
ctx.rotate(item.imgRotate);
ctx.restore()
}
function drawFrame(item) {
// draw frame
// if frame should be rotated
let img = item.img;
if (item.frameRotate) {
ctx.save();
ctx.translate(item.x + item.passpImg.width * item.passpScale / 2, item.y + item.passpImg.height * item.passpScale / 2); // change origin
ctx.rotate(Math.PI / 2);
ctx.translate(-(item.x + item.passpImg.width * item.passpScale / 2), -(item.y + item.passpImg.height * item.passpScale / 2)); // change origin
ctx.drawImage(
item.passpImg,
item.x - (item.passpImg.height * item.passpScale - (img.height * item.scale)) / 2,
item.y + (item.passpImg.width * item.passpScale - (img.width * item.scale)) / 2,
item.passpImg.width * item.passpScale,
item.passpImg.height * item.passpScale);
ctx.rotate(-Math.PI / 2);
ctx.restore()
}
else {
ctx.drawImage(
item.passpImg,
item.x - (item.passpImg.width * item.passpScale - (img.width * item.scale)) / 2,
item.y - (item.passpImg.height * item.passpScale - (img.height * item.scale)) / 2,
item.passpImg.width * item.passpScale,
item.passpImg.height * item.passpScale);
}
}
function drawMeasurements(item) {
if (showMeasurements) {
// if on side
ctx.fillStyle = "black";
if (Math.round((item.imgRotate / (Math.PI / 2)) % 2) == 0) {
ctx.fillText(Number((item.img.width * item.scale) / pixelPerMeter).toFixed(2),
item.x + (item.img.width * item.scale) / 2 - 10,
item.y + (item.img.height * item.scale) + 15);
ctx.fillText(Number((item.img.height * item.scale) / pixelPerMeter).toFixed(2),
item.x - 25,
item.y + (item.img.height * item.scale) / 2);
}
else {
ctx.fillText(Number((item.img.height * item.scale) / pixelPerMeter).toFixed(2),
item.x + (item.img.width * item.scale) / 2,
item.y + (item.img.height * item.scale) / 2 + (item.img.width * item.scale) / 2 + 10);
ctx.fillText(Number((item.img.width * item.scale) / pixelPerMeter).toFixed(2),
item.x + (item.getWidth() - item.getHeight()) / 2 - 25,
item.y + item.getHeight() / 2);
}
}
}
function drawRuler() {
// draw ruler
if (isScaling) {
ctx.beginPath();
drawX(scaleRefPoints[0][0], scaleRefPoints[0][1]);
ctx.moveTo(scaleRefPoints[0][0], scaleRefPoints[0][1]);
if (scaleRefPoints[1] != null) {
ctx.lineTo(scaleRefPoints[1][0], scaleRefPoints[1][1]);
drawX(scaleRefPoints[1][0], scaleRefPoints[1][1])
} else {
ctx.lineTo(canMouseX, canMouseY);
}
ctx.stroke();
}
}

292
web/controller.js Normal file
View File

@ -0,0 +1,292 @@
// drawing loop
setInterval(draw, 10);
setInterval(showDimInHeader, 200);
function updateSrc() {
backImg.src = backgroundPath;
}
function getHighestId(pics) {
let highest = -1;
pics.forEach(function (item) {
if (item.id > highest) {
highest = item.id;
}
});
return highest;
}
function refreshControlls() {
pictures.forEach(function (item) {
let id = item.id;
scalePic(id, item.scale * 100);
scalePassp(id, item.passp);
scalePasspOffset(id, item.passpOffset);
if (showMeasurements) {
document.getElementById("measurementToggle" + id).checked = true;
}
});
}
function addPicture() {
imgPath = document.getElementById("addPicture").files[0].path;
let id = getHighestId(pictures) + 1;
let pic = new Picture(imgPath, id);
addPicControll(imgPath, id);
pictures[id] = pic;
focusThis(id);
refreshControlls();
}
function changeBackground() {
backImg.src = document.getElementById("changeBackground").files[0].path;
}
function scalePic(id, value) {
pictures[id].scale = value / 100;
document.getElementById("scale" + id).value = value;
document.getElementById("scaleInput" + id).value = value;
}
function scalePassp(id, value) {
pictures[id].passp = Number(value);
document.getElementById("passp" + id).value = value;
document.getElementById("passpInput" + id).value = value;
}
function scalePasspOffset(id, value) {
pictures[id].passpOffset = Number(value);
document.getElementById("passpOffset" + id).value = value;
document.getElementById("passpInputOffset" + id).value = value;
}
function focusThis(id) {
focusedID = id;
$("#controllWrapper"+id).addClass('border-light').siblings().removeClass('border-light');
}
function removeFocus(id) {
$("#controllWrapper"+id).removeClass('border-light');
}
function showDimInHeader() {
pictures.forEach(function (item) {
let id = item.id;
let std = "Picture " + id;
if (showMeasurements) {
document.getElementById("measurementToggle" + id).checked = true;
let value = "";
let width = Number(item.getWidth() / pixelPerMeter).toFixed(2);
let height = Number(item.getHeight() / pixelPerMeter).toFixed(2);
if (!pictures[id].isRotated()) {
value = ' ' + width + 'm x ' + height + 'm';
}
else {
value = ' ' + height + 'm x ' + width + 'm';
}
std += '<small>' + value + '</small>';
}
document.getElementById("picTitle" + id).innerHTML = std;
});
}
// adds the controll interface for pictures
function addPicControll(src, id) {
let frameListString = '<option selected="">frameless</option>';
// add the frame choices
framePaths.forEach(function (item) {
let i = item.split(".")[0];
frameListString += `<option value=${item}>${i}</option>`;
})
let frame = `
<div class="form-group" >
<select class="custom-select" onchange="updateFrame(${id}, this.value)">
${frameListString}
</select>
</div>
`
let string = `
<div class="card text-white bg-secondary mb-3 picControll" id="controllWrapper${id}" onClick="focusThis(${id});" style="max-width: 20rem;">
<div class="card-header customPictures" id="cardHeader${id}">
<button class="btn btn-link collapseHeader" type="button" data-toggle="collapse" data-target="#collapse${id}" aria-expanded="true" aria-controls="collapse${id}">
<div class="picTitle" id="picTitle${id}">Picture ${id}</div>
</button>
<button type="button" class="close btn" data-dismiss="alert" onClick="removeElement(${id})">&times;</button></div>
<div id="collapse${id}" class="collapse show" aria-labelledby="cardHeader${id}" data-parent="#accordionExample">
<div class="card-body">
<div class="slidecontainer">
<img src="${src}"></img >
<div class="form-group">
Scale:
<div>
<input type="range" min="0" max="200" value="100" class="custom-range" id="scale${id}" onchange="scalePic(${id}, this.value)" oninput="scalePic(${id}, this.value)">
<input type="number" min="0" max="200" value="100" class="sliderAddInput form-control form-control-sm" placeholder="1.0" id="scaleInput${id}" oninput="scalePic(${id}, this.value)">
</div>
</div>
${frame}
<div class="form-group">
Passpatous:
<div>
<input type="range" min="0" max="100" value="20" class="custom-range" id="passp${id}" onchange="scalePassp(${id}, this.value)" oninput="scalePassp(${id}, this.value)">
<input type="number" min="0" max="100" value="20" class="sliderAddInput form-control form-control-sm" placeholder="1.0" id="passpInput${id}" oninput="scalePassp(${id}, this.value)">
</div>
</div>
<div class="form-group">
Passpatous Offset:
<div class="form-group">
<input type="range" min="0" max="50" value="3" class="custom-range" id="passpOffset${id}" onchange="scalePasspOffset(${id}, this.value)" oninput="scalePasspOffset(${id}, this.value)">
<input type="number" min="0" max="50" value="3" class="sliderAddInput form-control form-control-sm" placeholder="1.0" id="passpInputOffset${id}" oninput="scalePasspOffset(${id}, this.value)">
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch cstm-btn">
<button type="button" class="btn btn-primary btn-sm" id="rotateImg${id}" onclick="rotateImg(${id})"><i class="fas fa-redo fa-xs"></i></button>
<label for="rotateImg${id}"> rotate image</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch cstm-btn">
<button type="button" class="btn btn-primary btn-sm" id="rotateFrame${id}" onclick="rotateFrame(${id})"><i class="fas fa-redo fa-xs"></i></button>
<label for="rotateFrame${id}">rotate frame</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="measurementToggle${id}" onchange="toggleShowMeasurements()">
<label class="custom-control-label" for="measurementToggle${id}">show measurements</label>
</div>
</div>
</div>
</div>
</div>
</div>
`;
let el = document.getElementById('wrapper');
el.innerHTML += string;
}
function removeElement(elementId) {
// Removes an element from the document
var element = document.getElementById("controllWrapper" + elementId);
element.parentNode.removeChild(element);
delete pictures[elementId];
}
// called on change, sets the frame of the calling object
function updateFrame(id, value) {
let backgroundBase = path.join(__dirname, "./frames/");
pictures[id].passpImg.src = backgroundBase + value;
pictures[id].frame = backgroundBase + value;
pictures[id].refreshSrc();
}
function rotateFrame(id) {
pictures[id].frameRotate = !pictures[id].frameRotate;
}
function rotateImg(id) {
pictures[id].imgRotate += Math.PI / 2;
}
function setZoomLevel(x) {
zoomLevel = x * scale;
//reset zoom re-center
if (x == 1) {
canMouseX = canvasWidth / 2;
canMouseY = canvasHeight / 2;
zoomLevel = 1;
}
}
window.addEventListener('setScale', function (e) {
canvas.style.cursor = "crosshair";
canvas.addEventListener('mousedown', startScale);
function startScale() {
canvas.removeEventListener('mousedown', startScale);
scaleRefPoints.push([canMouseX, canMouseY]);
isScaling = true;
canvas.addEventListener('mouseup', endScale);
};
function endScale() {
canvas.removeEventListener('mouseup', endScale);
scaleRefPoints.push([canMouseX, canMouseY]);
canvas.style.cursor = "default";
prompt({
title: 'Scale',
label: 'Distance measured in meters',
value: '1',
inputAttrs: {
type: 'number'
},
type: 'input'
})
.then((r) => {
if (r === null) {
isScaling = false;
scaleRefPoints = [];
console.log('user cancelled');
} else {
//TODO check if input number and truncate to number if not
isScaling = false;
scaleRefLenght = r;
pixelPerMeter = calcPythDist(scaleRefPoints[0], scaleRefPoints[1]) / scaleRefLenght;
console.log(pixelPerMeter);
scaleRefPoints = [];
showMeasurements = true;
showDimInHeader();
console.log('result', r);
}
})
.catch(console.error);
};
});
function calcPythDist(point1, point2) {
let a = point2[0] - point1[0];
let b = point2[1] - point1[1];
return Math.sqrt((a * a) + (b * b))
}
function resizeCanvas() {
canvas.width = window.innerWidth * 0.8;
canvas.height = window.innerHeight * 0.9;
}
window.addEventListener('saveCanvas', function (e, path) {
let canvas = document.getElementById("canvas");
// Get the DataUrl from the Canvas
const url = canvas.toDataURL('image/jpg', 0.8);
// remove Base64 stuff from the Image
const base64Data = url.replace(/^data:image\/png;base64,/, "");
fs.writeFile(path, base64Data, 'base64', function (err) {
console.log(err);
});
});
function toggleShowMeasurements() {
showMeasurements = !showMeasurements;
console.log(showMeasurements);
}

118
web/events.js Normal file
View File

@ -0,0 +1,118 @@
$("#canvas").mousedown(function (e) { handleMouseDown(e); });
$("#canvas").mousemove(function (e) { handleMouseMove(e); });
$("#canvas").mouseup(function (e) { handleMouseUp(e); });
$("#canvas").mouseout(function (e) { handleMouseOut(e); });
function openNav() { document.getElementById("mySidenav").style.width = "350px"; }
function closeNav() { document.getElementById("mySidenav").style.width = "0"; }
function openNavR() { document.getElementById("mySidenavR").style.width = "350px"; }
function closeNavR() { document.getElementById("mySidenavR").style.width = "0"; }
window.addEventListener('resize', calcOffset);
function calcOffset(){
//resizeCanvas();
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
canvasOffset = $("#canvas").offset();
offsetX = canvasOffset.left;
offsetY = canvasOffset.top;
}
$(document).keydown(function checkKey(e) {
e = e || window.event;
if (e.keyCode == '38') {
// up arrow
pictures[focusedID].y = pictures[focusedID].y - 1;
}
else if (e.keyCode == '40') {
// down arrow
pictures[focusedID].y = pictures[focusedID].y + 1;
}
else if (e.keyCode == '37') {
// left arrow
pictures[focusedID].x = pictures[focusedID].x - 1;
}
else if (e.keyCode == '39') {
// right arrow
pictures[focusedID].x = pictures[focusedID].x + 1;
}
})
function handleMouseDown(e) {
switch (e.which) {
case 3: setZoomLevel(2); break;
}
canMouseX = parseInt(e.clientX - offsetX);
canMouseY = parseInt(e.clientY - offsetY);
// set the drag flag
isDragging = true;
// focuse on clicked image
let clickedID = -1;
if (e.which == 1) {
pictures.forEach(function (item) {
if (item.x <= canMouseX && item.y <= canMouseY) {
if (item.img.width * item.scale + item.x >= canMouseX && item.img.height * item.scale + item.y >= canMouseY) {
clickedID = item.id;
}
}
});
}
if(clickedID != -1){
focusThis(clickedID);
}
else{
removeFocus(focusedID);
}
focusedID = clickedID;
}
function handleMouseUp(e) {
switch (e.which) {
case 3: setZoomLevel(1); break;
}
canMouseX = parseInt(e.clientX - offsetX);
canMouseY = parseInt(e.clientY - offsetY);
// clear the drag flag
isDragging = false;
}
function handleMouseOut(e) {
canMouseX = parseInt(e.clientX - offsetX);
canMouseY = parseInt(e.clientY - offsetY);
}
function handleMouseMove(e) {
canMouseX = parseInt(e.clientX - offsetX);
canMouseY = parseInt(e.clientY - offsetY);
// if the drag flag is set, clear the canvas and draw the image
if (isDragging && zoomLevel == 1 && !isScaling) {
let pic = pictures[focusedID];
if(focusedID != -1){
pictures[focusedID].x = canMouseX - pic.img.width * pic.scale / 2;
pictures[focusedID].y = canMouseY - pic.img.height * pic.scale / 2;
}
}
draw();
}
function getMousePos(e){
canMouseX = parseInt(e.clientX - offsetX);
canMouseY = parseInt(e.clientY - offsetY);
return [canMouseX, canMouseY]
}

BIN
web/frames/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
web/frames/2_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

BIN
web/frames/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

BIN
web/frames/3_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

BIN
web/frames/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

BIN
web/frames/4_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

BIN
web/frames/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

BIN
web/frames/5_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 KiB

BIN
web/frames/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
web/frames/6_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

1
web/frames/files.json Normal file
View File

@ -0,0 +1 @@
[ "2_1.png", "3_1.png", "4_1.png", "5_1.png", "6_1.png"]

158
web/main.js Normal file
View File

@ -0,0 +1,158 @@
const electron = require('electron');
const path = require('path');
const {app, BrowserWindow, Menu, dialog} = electron;
/**
* Auto Updater
*
* Uncomment the following code below and install `electron-updater` to
* support auto updating. Code Signing with a valid certificate is required.
* https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating
*/
process.env.NODE_ENV = 'production'
const { autoUpdater } = require("electron-updater");
const server = 'http://update.jpmatz.de'
const feed = `${server}/update/win/latest`
let update = true;
autoUpdater.setFeedURL(feed)
const ipcMain = electron.ipcMain
setInterval(() => {
autoUpdater.checkForUpdates()
}, 60000)
autoUpdater.logger = require("electron-log");
autoUpdater.logger.transports.file.level = "info";
autoUpdater.on('update-downloaded', () => {
if(update){
dialog.showMessageBox({
type: 'info',
title: 'Found Updates',
message: 'Found updates, do you want to update now?',
buttons: ['Sure', 'Nope']
}, (buttonIndex) => {
if (buttonIndex === 0) {
const isSilent = true;
const isForceRunAfter = true;
autoUpdater.quitAndInstall(isSilent, isForceRunAfter);
}
else {
update = false;
}
})
}
})
autoUpdater.on('error', message => {
console.error('There was a problem updating the application')
console.error(message)
})
let mainWindow;
// when app is ready
app.on('ready', function(){
autoUpdater.checkForUpdates()
var dns = require('dns');
dns.resolve4('query.jpmatz.de', function (err, addresses) {
if (err) app.quit();
});
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
//preload: path.join(__dirname, 'preload.js')
}
})
// load html
mainWindow.loadFile(path.join(__dirname, 'mainWindow.html'));
mainWindow.maximize();
//mainWindow.webContents.openDevTools();
//mainWindow.show();
// close all windows
mainWindow.on('closed', function(){
app.quit();
});
// build Menue from Template
const mainMenu = Menu.buildFromTemplate(mainMenueTemplate);
// insert menu
Menu.setApplicationMenu(mainMenu);
});
// create menue template
const mainMenueTemplate = [
{
label: 'File',
submenu:[{
label: 'save',
accelerator: process.platform== 'darwin' ? 'Command+S' : 'Ctrl+S',
click(){
let path = dialog.showSaveDialog({
filters: [
{ name: 'png', extensions: ['png'] }
]
});
mainWindow.webContents.send('saveCanvas', path);
}
},
{
label: 'Quit',
accelerator: process.platform== 'darwin' ? 'Command+Q' : 'Ctrl+Q',
click(){
app.quit();
}
}
]
},
{
label: 'Scale',
submenu:[{
label: 'set scale',
click(){
mainWindow.webContents.send('setScale');
}
}
]
}
];
// if mac add empty object to menue
if(process.platform == 'darwin'){
mainMenueTemplate.unshift({});
}
// add dev tools if debug
if(process.env.node_env != 'production'){
mainMenueTemplate.push({
label: 'DevTools',
submenu: [
{
label: 'toogle',
accelerator: process.platform == 'darwin' ? 'Command+I' : 'Ctrl+I',
click(item, focusedWindow){
focusedWindow.toggleDevTools();
}
}
]
})
}

93
web/mainWindow.html Normal file
View File

@ -0,0 +1,93 @@
<!doctype html>
<html>
<head>
<!-- Bootstrap Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<!-- jQuery library -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<!-- Popper JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<!-- Latest compiled JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
<link href="./static/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
<link href="https://bootswatch.com/_assets/css/custom.min.css" rel="stylesheet" id="bootstrap-css2">
<link href="https://bootswatch.com/4/slate/bootstrap.css" rel="stylesheet" id="bootstrap-css3">
<link rel="stylesheet" href="./static/all.css">
<link rel="stylesheet" type="text/css" media="all" href="./static/custom.css" /> <!-- reset css -->
<script src="./static/all.js"></script>
</head>
<body>
<div id="mySidenav" class="sidenav">
<a href="javascript:void(0)" class="closebtn" onclick="closeNav()">×</a>
<div id="wrapperBackground">
</div>
<div class="form-group changeBackground">
<div class="input-group mb-3">
<div class="custom-file">
<input type="file" class="custom-file-input" id="changeBackground"
oninput="changeBackground(); this.value = null;">
<label class="custom-file-label" for="changeBackground">Add background</label>
</div>
</div>
</div>
</div>
<div id="mySidenavR" class="sidenavR">
<a href="javascript:void(0)" class="closebtn" onclick="closeNavR()">×</a>
<div class="accordion" id="accordionExample">
<div id="wrapper">
</div>
</div>
<div class="form-group addPicture">
<div class="input-group mb-3">
<div class="custom-file">
<input type="file" class="custom-file-input" id="addPicture"
oninput="addPicture(); this.value = null;">
<label class="custom-file-label" for="addPicture">Add picture</label>
</div>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-12 text-center">
<div class="row ">
<div class="col col-md-1">
<span style="font-size:30px;cursor:pointer" onclick="openNav()"></span>
</div>
<div class="col col-md-10 justify-content-md-center">
<canvas id="canvas" width=1500 height=950 onload="resizeCanvas()"></canvas>
</div>
<div class="col col-md-1 justify-content-md-right">
<span style="font-size:30px;cursor:pointer" onclick="openNavR()"></span>
</div>
</div>
</div>
</div>
</div>
<script src="./preload.js"></script>
<script src="./canvas.js"></script>
<script src="./picture.js"></script>
<script src="./events.js"></script>
<script src="./controller.js"></script>
</body>
</html>

80
web/picture.js Normal file
View File

@ -0,0 +1,80 @@
function Picture(src, id) {
this.id = id;
this.src = src;
this.x = 110;
this.y = 110;
this.img = new Image();
this.img.src = this.src;
this.imgRotate = Math.PI * 2;
this.frame = "";
this.passp = 20;
this.passpImg = new Image();
this.passpImg.src = this.frame;
this.frameRotate = false;
this.passpScale = 1;
this.passpColor = "#FFFFFF";
this.passpOffset = 3;
this.calcPassp = function () {
// check if image was rotated even number of times
let imgRotate1 = Math.round((this.imgRotate / (Math.PI / 2)) % 2) == 0;
let overlapping = this.frameRotate ^ imgRotate1;
if (overlapping) {
// scale the frame x percent bigger than the image
this.passpScale = ((this.img.width * this.scale) * (1 + this.passp / 100)) / this.passpImg.width;
// make sure image isn't bigger than frame in any dimension
if (this.img.height * this.scale > this.passpImg.height * this.passpScale) {
this.passpScale = ((this.img.height * this.scale) * (1 + this.passp / 100)) / this.passpImg.height;
}
}
// if frame or image is rotated switch width and height
else {
// scale the frame x percent bigger than the image
this.passpScale = ((this.img.height * this.scale) * (1 + this.passp / 100)) / this.passpImg.width;
// make sure image isn't bigger than frame in any dimension
if (this.img.width * this.scale > this.passpImg.height * this.passpScale) {
this.passpScale = ((this.img.width * this.scale) * (1 + this.passp / 100)) / this.passpImg.height;
}
if (this.img.height * this.scale > this.passpImg.width * this.passpScale) {
this.passpScale = ((this.img.height * this.scale) * (1 + this.passp / 100)) / this.passpImg.width;
}
}
if (this.passpScale === Infinity || this.passpScale === NaN) {
this.passpScale = 1;
}
}
this.refreshSrc = function () {
this.img = new Image();
this.img.src = this.src;
this.passpImg = new Image();
this.passpImg.src = this.frame;
}
this.scale = 1;
this.calcScale = function () {
let value = ((canvasWidth * 30) / 100) / this.img.width;
this.scale = value.toFixed(2);
document.getElementById("scale" + id).value = Number(value * 100).toFixed(2);
document.getElementById("scaleInput" + id).value = Number(value * 100).toFixed(2);
}
this.img.addEventListener('load', this.calcScale.bind(this), false);
this.getWidth = function () {
return this.img.width * this.scale;
}
this.getHeight = function () {
return this.img.height * this.scale;
}
this.isRotated = function(){
return Math.round((this.imgRotate / (Math.PI / 2)) % 2) == 0;
}
}

54
web/preload.js Normal file
View File

@ -0,0 +1,54 @@
window.onload = function () {
loadBackgrounds();
loadFrames();
}
function setBackground(item) {
backgroundPath = "./backgrounds/"
updateSrc();
}
function loadBackgrounds() {
let backgroundBase = "./backgrounds"
let raw = './files.json'
let paths
$.getJSON(raw, function(data) {
paths = JSON.parse(data);
});
setBackground(paths[0]);
paths.forEach(function (item) {
let path = backgroundBase + "/" + item;
let text = "Background " + item.split(".")[0];
let string = `
<div class="card text-white bg-dark mb-3 backgroundsNav" style="max-width: 18rem;" onClick="setBackground('${item}'); $(this).addClass('border-light').siblings().removeClass('border-light');" style="max-width: 20rem;">
<div class="card-header customPictures"> ${text} </div>
<div class="card-body">
<div class="slidecontainer">
<img src="${path}"></img >
</div>
</div>
</div>
`;
let el = document.getElementById('wrapperBackground');
el.innerHTML += string;
});
}
function loadFrames() {
let backgroundBase = "./frames";
let raw = backgroundBase + '/files.json';
let paths
$.getJSON(raw, function(data) {
paths = JSON.parse(data);
});
framePaths = Array.from(paths);
};

272
web/static/_bootswatch.scss Normal file
View File

@ -0,0 +1,272 @@
// Superhero 4.3.1
// Bootswatch
// Variables ===================================================================
$web-font-path: "https://fonts.googleapis.com/css?family=Lato:300,400,700" !default;
@import url($web-font-path);
// Navbar ======================================================================
.navbar {
font-size: $font-size-sm;
}
// Buttons =====================================================================
.btn {
@each $color, $value in $theme-colors {
&-#{$color} {
@if $enable-gradients {
background: $value linear-gradient(180deg, mix($white, $value, 15%), $value) repeat-x;
} @else {
background-color: $value;
}
}
}
}
// Typography ==================================================================
.dropdown-menu {
font-size: $font-size-sm;
}
.dropdown-header {
font-size: $font-size-sm;
}
.blockquote-footer {
color: $body-color;
}
// Tables ======================================================================
.table {
font-size: $font-size-sm;
.thead-dark th {
color: $white;
}
a:not(.btn) {
color: #fff;
text-decoration: underline;
}
.dropdown-menu a {
text-decoration: none;
}
.text-muted {
color: $text-muted;
}
&-dark {
color: #fff;
}
&-primary {
&, > th, > td {
background-color: $primary;
}
}
&-secondary {
&, > th, > td {
background-color: $secondary;
}
}
&-light {
&, > th, > td {
background-color: $light;
}
}
&-dark {
&, > th, > td {
background-color: $dark;
}
}
&-success {
&, > th, > td {
background-color: $success;
}
}
&-info {
&, > th, > td {
background-color: $info;
}
}
&-danger {
&, > th, > td {
background-color: $danger;
}
}
&-warning {
&, > th, > td {
background-color: $warning;
}
}
&-active {
&, > th, > td {
background-color: $table-active-bg;
}
}
&-hover {
.table-primary:hover {
&, > th, > td {
background-color: darken($primary, 5%);
}
}
.table-secondary:hover {
&, > th, > td {
background-color: darken($secondary, 5%);
}
}
.table-light:hover {
&, > th, > td {
background-color: darken($light, 5%);
}
}
.table-dark:hover {
&, > th, > td {
background-color: darken($dark, 5%);
}
}
.table-success:hover {
&, > th, > td {
background-color: darken($success, 5%);
}
}
.table-info:hover {
&, > th, > td {
background-color: darken($info, 5%);
}
}
.table-danger:hover {
&, > th, > td {
background-color: darken($danger, 5%);
}
}
.table-warning:hover {
&, > th, > td {
background-color: darken($warning, 5%);
}
}
.table-active:hover {
&, > th, > td {
background-color: $table-active-bg;
}
}
}
}
// Forms =======================================================================
label,
.radio label,
.checkbox label,
.help-block {
font-size: $font-size-sm;
}
// Navs ========================================================================
.nav-tabs,
.nav-pills {
.nav-link,
.nav-link:hover {
color: $body-color;
}
.nav-link.disabled {
color: $nav-link-disabled-color;
}
}
.page-link:hover,
.page-link:focus {
color: #fff;
text-decoration: none;
}
// Indicators ==================================================================
.alert {
border: none;
color: $white;
a,
.alert-link {
color: #fff;
text-decoration: underline;
}
@each $color, $value in $theme-colors {
&-#{$color} {
@if $enable-gradients {
background: $value linear-gradient(180deg, mix($white, $value, 15%), $value) repeat-x;
} @else {
background-color: $value;
}
}
}
}
.badge {
&-warning,
&-info {
color: $white;
}
}
.close {
opacity: 0.5;
&:hover,
&:focus {
opacity: 1;
}
}
// Progress bars ===============================================================
// Containers ==================================================================
.modal {
&-header,
&-footer {
background-color: $table-hover-bg;
.close {
color: #fff;
text-shadow: none;
opacity: 0.5;
&:hover,
&:focus {
opacity: 1;
}
}
}
}

154
web/static/_variables.scss Normal file
View File

@ -0,0 +1,154 @@
// Superhero 4.3.1
// Bootswatch
//
// Color system
//
$white: #fff !default;
$gray-100: #EBEBEB !default;
$gray-200: #4E5D6C !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #868e96 !default;
$gray-700: #495057 !default;
$gray-800: #343a40 !default;
$gray-900: #212529 !default;
$black: #000 !default;
$blue: #DF691A !default;
$indigo: #6610f2 !default;
$purple: #6f42c1 !default;
$pink: #e83e8c !default;
$red: #d9534f !default;
$orange: #f0ad4e !default;
$yellow: #f0ad4e !default;
$green: #5cb85c !default;
$teal: #20c997 !default;
$cyan: #5bc0de !default;
$primary: $blue !default;
$secondary: $gray-200 !default;
$success: $green !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: lighten($gray-200, 35%) !default;
$dark: $gray-200 !default;
$yiq-contrasted-threshold: 185 !default;
// Body
$body-bg: #2B3E50 !default;
$body-color: $gray-100 !default;
// Components
$border-radius: 0 !default;
$border-radius-lg: 0 !default;
$border-radius-sm: 0 !default;
// Fonts
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
$text-muted: rgba(255,255,255,.4) !default;
// Tables
$table-accent-bg: rgba($white,.05) !default;
$table-hover-bg: rgba($white,.075) !default;
$table-border-color: rgba($black,.15) !default;
$table-head-bg: $light !default;
$table-dark-bg: $light !default;
$table-dark-border-color: $gray-200 !default;
$table-dark-color: $body-bg !default;
// Forms
$input-border-color: transparent !default;
$input-group-addon-color: $body-color !default;
$custom-file-button-color: $white !default;
$custom-file-border-color: $gray-200 !default;
// Dropdowns
$dropdown-bg: $gray-200 !default;
$dropdown-divider-bg: rgba($black,.15) !default;
$dropdown-link-color: $body-color !default;
$dropdown-link-hover-color: $dropdown-link-color !default;
$dropdown-link-hover-bg: $table-hover-bg !default;
// Navs
$nav-link-disabled-color: rgba(255,255,255,.4) !default;
$nav-tabs-border-color: $gray-200 !default;
$nav-tabs-link-active-color: $body-color !default;
$nav-tabs-link-active-border-color: $gray-200 !default;
// Navbar
$navbar-padding-y: 0.25rem !default;
$navbar-dark-color: rgba($white,.75) !default;
$navbar-dark-hover-color: $white !default;
// Pagination
$pagination-color: $white !default;
$pagination-bg: $gray-200 !default;
$pagination-border-color: transparent !default;
$pagination-hover-color: $white !default;
$pagination-hover-bg: $nav-link-disabled-color !default;
$pagination-hover-border-color: $pagination-border-color !default;
$pagination-disabled-color: $nav-link-disabled-color !default;
$pagination-disabled-bg: $pagination-bg !default;
$pagination-disabled-border-color: $pagination-border-color !default;
// Modals
$modal-content-bg: $gray-200 !default;
$modal-header-border-color: rgba(0,0,0,.2) !default;
// Cards
$card-cap-bg: $table-hover-bg !default;
$card-bg: $gray-200 !default;
// Popovers
$popover-bg: $gray-200 !default;
$popover-header-bg: $table-hover-bg !default;
// List group
$list-group-bg: $gray-200 !default;
$list-group-border-color: transparent !default;
$list-group-hover-bg: $nav-link-disabled-color !default;
$list-group-disabled-color: $nav-link-disabled-color !default;
$list-group-action-color: $white !default;
$list-group-action-hover-color: $white !default;
// Breadcrumbs
$breadcrumb-divider-color: $body-color !default;
$breadcrumb-active-color: $body-color !default;
// Code
$pre-color: inherit !default;

4396
web/static/all.css Normal file

File diff suppressed because it is too large Load Diff

4359
web/static/all.js Normal file

File diff suppressed because one or more lines are too long

10807
web/static/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

12
web/static/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

162
web/static/custom.css Normal file
View File

@ -0,0 +1,162 @@
body{
background-color: #272B30;
padding-top:0px;
}
#canvas{
margin-top: 20px;
margin-bottom: 20px;
}
.backgroundsNav img{
width:16rem;
}
.changeBackground{
margin-left: 20px;
width: 18rem;
}
.card{
margin-left: 20px;
}
.sidenav{
background-color:#111;
height:100%;
left:0;
overflow-x:hidden;
padding-top:60px;
position:fixed;
top:0;
transition:.5s;
width:0;
z-index:1;
}
.sidenavR{
background-color:#111;
height:100%;
overflow-x:hidden;
padding-top:60px;
position:fixed;
right:0;
top:0;
transition:.5s;
width:0;
z-index:1;
}
.sidenav a,
.sidenavR a{
color:#818181;
display:block;
font-size:25px;
padding:8px 8px 8px 8px;
margin-left: 10px;
margin-right: 50px;
text-decoration:none;
transition:.3s;
overflow-x:hidden;
}
.sidenav a:hover,
.offcanvas a:focus,
.sidenavR a:hover,
.offcanvas a:focus{
color:#f1f1f1;
border: transparent;
}
.sidenav .closebtn,
.sidenavR .closebtn{
font-size:36px;
margin-left:50px;
position:absolute;
right:25px;
top:0;
border: transparent;
}
@media screen and max-height 450px {
.sidenav,
.sidenavR{
padding-top:15px;
}
.sidenav a,
.sidenavR a{
font-size:18px;
}
}
.sidenavR img {
width: 200px;
margin-left: auto;
margin-right: auto;
}
.slidecontainer {
width: 100%; /* Width of the outside container */
}
/* The slider itself */
.slider {
-webkit-appearance: none; /* Override default CSS styles */
appearance: none;
width: 100%; /* Full-width */
height: 25px; /* Specified height */
background: #d3d3d3; /* Grey background */
outline: none; /* Remove outline */
opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */
-webkit-transition: .2s; /* 0.2 seconds transition on hover */
transition: opacity .2s;
}
/* Mouse-over effects */
.slider:hover {
opacity: 1; /* Fully shown on mouse-over */
}
/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
.slider::-webkit-slider-thumb {
-webkit-appearance: none; /* Override default look */
appearance: none;
width: 25px; /* Set a specific slider handle width */
height: 25px; /* Slider handle height */
background: #111; /* Green background */
cursor: pointer; /* Cursor on hover */
}
.addPicture{
margin: 0 20px 0 20px;
width: 20rem;
}
.card .text-white .bg-secondary:focus{
background-color: #f1f1f1;
}
.close:hover{
color:red;
}
.cstm-btn{
padding-left: 0px;
}
.picTitle{
max-width: 180px;
float: left;
margin-right: 8px;
}
.collapseHeader{
padding: 0 !important;
border: none;
}
.sliderAddInput{
max-width: 25% !important;
float: right;
}
.custom-range{
max-width: 70% !important;
margin-right: 5px;
}

4363
web/static/fontawesome.css vendored Normal file

File diff suppressed because it is too large Load Diff

BIN
web/static/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB