Compare commits

...

3 Commits

Author SHA1 Message Date
alban
60b9129e7c [enh] Adds picture upload + code cleanup (a bit) 2020-11-05 16:50:49 +01:00
alban
d0288d3cc9 [fix] Reworks Docker 2020-11-05 16:50:20 +01:00
alban
d1f1c32c03 [fix] tweaks 2020-11-05 09:59:53 +01:00
6 changed files with 213 additions and 163 deletions

View File

@ -15,10 +15,10 @@ RUN rm -f /etc/nginx/sites-enabled/*
RUN pip3 install flask numpy pillow redis RUN pip3 install flask numpy pillow redis
RUN pip3 install pypotrace RUN pip3 install pypotrace
COPY ./files/nginx/sites-enabled/site.conf /etc/nginx/sites-enabled COPY ./server/files/nginx/sites-enabled/site.conf /etc/nginx/sites-enabled
COPY . . COPY . .
COPY entrypoint.sh /usr/bin/ COPY ./server/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh RUN chmod +x /usr/bin/entrypoint.sh
EXPOSE 80 EXPOSE 80

View File

@ -13,19 +13,7 @@
#canvas { display: block; } #canvas { display: block; }
span.ui-slider-handle.ui-corner-all.ui-state-default { background: dodgerblue;} span.ui-slider-handle.ui-corner-all.ui-state-default { background: dodgerblue;}
canvas#simuCanvas {background: #333;} canvas#simuCanvas {background: #333;}
div.select { .row {margin: 24px 0;}
display: inline-block;
margin: 0 0 1em 0;
}
p.small {
font-size: 0.7em;
}
label {
width: 12em;
display: inline-block;
}
</style> </style>
<link rel="stylesheet" type="text/css" href="css/bootstrap/css/bootstrap.min.css" /> <link rel="stylesheet" type="text/css" href="css/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="js/jquery-ui/jquery-ui.css" /> <link rel="stylesheet" type="text/css" href="js/jquery-ui/jquery-ui.css" />
@ -37,30 +25,38 @@
<div class="container-fluid container-md"> <div class="container-fluid container-md">
<div class="row"> <div class="row">
<div class="col-sm col-6"> <div class="col-sm-7">
<video id="video" class="video_sized" autoplay></video> <video id="video" class="video_sized" autoplay></video>
</div> </div>
<div class="col-sm"> <div class="col-sm-5">
<h1 class="text-primary">Convert your photos to laser images!</h1> <h1 class="text-primary">Convert your photos to laser images!</h1>
<p class="lead">Once you allow the browser to use your camera, you can start creating something new by taking a snapshot. </p> <p class="lead">Create something fresh by allowing the browser to use your camera.</p>
<p class="lead">Or pick an image from your phone and upload it. </p>
<p class="lead">If you're happy with the result, send it to see it converted to a laser-ready image! </p> <p class="lead">If you're happy with the result, send it to see it converted to a laser-ready image! </p>
<div class="select"> <div class="select">
<label for="videoSource">Change camera </label><select id="videoSource" class="custom-select"></select> <label for="videoSource">Change camera </label><select id="videoSource" class="custom-select"></select>
</div> </div>
<button class="snap btn btn-lg btn-primary">Take a snap</button> <div class="row">
<div class="col-6">
<button class="snap btn btn-block btn-lg btn-primary">Take a snap</button>
</div>
<div class="col-6">
<button id="uploadBtn" class=" btn btn-block btn-lg btn-primary">Upload a file</button>
<input id="uploadInput" type="file" multiple style="display: none"/>
</div>
</div>
</div> </div>
</div> </div>
<div class="row" id="result"> <div class="row result" id="result" style="display:none">
<div class="col-sm col-6"> <div class="col-sm-7">
<div class="video_sized"> <div class="video_sized">
<canvas id="canvas" class="video_sized"></canvas> <canvas id="canvas" class="video_sized"></canvas>
</div> </div>
</div> </div>
<div class="col-sm"> <div class="col-sm">
<h1 class="text-primary">Are you satisfied with the contrast? </h3> <h2 class="text-primary">Not satisfied with contrast? </h2>
<p>Before sending your snap, change the contrast using the slider to reach the effect you desire.</p><br/> <p class="lead">Before sending your snap, feel free to modify the contrast by using the slider to reach the effect you desire.</p>
<div id="slider"></div> <div id="slider"></div>
<br/>
<div class="float-left"> <div class="float-left">
<button type="button" class="btn btn-secondary contrast" rel="-5"> <button type="button" class="btn btn-secondary contrast" rel="-5">
<span class="badge badge-light">+</span> white <span class="badge badge-light">+</span> white
@ -70,27 +66,42 @@
<button type="button" class="btn btn-secondary contrast" rel="5"> <button type="button" class="btn btn-secondary contrast" rel="5">
<span class="badge badge-light">+</span> black <span class="badge badge-light">+</span> black
</button> </button>
</div><br> </div>
<div class="float-left">
<br/> <br />
<h3>Ready to send? </h3> <h2 class="text-primary">Not satisfied with frame?</h2>
<p class="lead">Before sending your snap, use the blue handles that appear over it to cut the edges. You can also zoom in.</p>
</div>
</div>
</div>
<div class="row result" style="display:none">
<div class="col-sm-7">
<h1 class="text-primary"> Got text?</h1>
<p >If some text is visible on the image, type it before pushing the Send button.</p> <p >If some text is visible on the image, type it before pushing the Send button.</p>
<div class="form-group"> <div class="form-group">
<input id="text" type="text" class="form-control form-control-lg" placeholder="Type text here"/> <input id="text" type="text" class="form-control form-control-lg" placeholder="Type text here"/>
</div>
</div>
<div class="col-sm-5">
<h1 class="text-success">Ready to send? </h1>
<div class="form-group">
<button id="send" class="form-control form-control-lg btn btn-lg btn-success btn-block">Send</button> <button id="send" class="form-control form-control-lg btn btn-lg btn-success btn-block">Send</button>
</div> </div>
</div>
</div>
<div class="row result" style="display:none">
<div class="col-sm">
<div id="messages"> <div id="messages">
</div> </div>
</div> </div>
</div> </div>
<div class="row" id="preview"> <div class="row" id="preview">
<div class="col-sm col-6"> <div class="col-sm-7">
<canvas id="simuCanvas" class="video_sized"></canvas> <canvas id="simuCanvas" class="video_sized"></canvas>
</div> </div>
<div class="col-sm" > <div class="col-sm-5" >
<h1 class="text-primary">Preview your creation</h1> <h2 class="text-secondary">Preview your creations</h2>
<p>Now that your creation have been sent, you can check what they will look like on lasers using these links, sorted by oldest snaps last.</p> <p class="lead">Now that your creation have been sent, you can check what they will look like on lasers using these links, sorted by oldest snaps last.</p>
<div id="imageList"></div> <div id="imageList"></div>
</div> </div>
@ -98,14 +109,12 @@
</div> </div>
</main>
<script src="js/jquery-3.5.1.min.js"></script> <script src="js/jquery-3.5.1.min.js"></script>
<script src="js/cropper/cropper.min.js"></script> <script src="js/cropper/cropper.min.js"></script>
<script src="js/jquery-ui/jquery-ui.js"></script> <script src="js/jquery-ui/jquery-ui.js"></script>
<script src="js/canvas.js"></script> <script src="js/main.js"></script>
<script src="js/adapter-latest.js"></script> <script src="js/adapter-latest.js"></script>
<script src="js/camera.js" async></script> <script src="js/camera.js" async></script>
</body> </body>
</html> </html>

View File

@ -1,10 +1,7 @@
$(document).ready(function(){ $(document).ready(function(){
var binary_level = 100
var url = document.location.origin
var cropper = null
var snap = null
Cropper.setDefaults({ Cropper.setDefaults({
viewMode: 1, viewMode: 1,
@ -12,66 +9,81 @@ $(document).ready(function(){
}) })
// Grab elements, create settings, etc. // Grab elements, create settings, etc.
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var video = document.getElementById('video'); var video = document.getElementById('video');
var mediaConfig = { video: true }; var snapCanvas = document.getElementById('canvas');
var snapContext = snapCanvas.getContext('2d');
var simuCanvas = document.getElementById("simuCanvas");
var simuContext = simuCanvas.getContext("2d");
var simuLastPoint = { x: 0, y: 0, color: 0};
var simuZoom = 1;
var binary_level = 100
var url = document.location.origin
var cropper = null
var snap = null
/*
Helpers
*/
var errBack = function(e) { var errBack = function(e) {
console.log('An error has occurred!', e) console.log('An error has occurred!', e)
}; };
var Point = function( l ){
return {
"x" : l[0],
"y" : l[1],
"color" : l[2]
}
}
/** /**
Take a new snapshot from the camera and store it Take a new snapshot from the camera and store it
**/ **/
function doSnap(){ function videoToSnap(){
showSnap(video, video.width, video.height)
}
function showSnap(src, width, height){
cropper && cropper.destroy() cropper && cropper.destroy()
let width = video.width snapContext.drawImage(src, 0, 0, width, height);
let height = video.height snap = snapContext.createImageData(width, height);
context.drawImage(video, 0, 0, width, height); snap.data.set( snapContext.getImageData(0, 0, width, height).data )
snap = context.createImageData(width, height); binary(snapContext)
snap.data.set( context.getImageData(0, 0, width, height).data ) $(".result").show()
binary(context) cropper = new Cropper(snapCanvas);
$("#result").show()
// Animation removed: it was causing problems with the send button. Fix. // Animation removed: it was causing problems with the send button. Fix.
//$('html, body').animate({scrollTop: $("#result").offset().top}, 2000); //$('html, body').animate({scrollTop: $("#result").offset().top}, 2000);
cropper = new Cropper(canvas);
} }
function doContrast(){ function doContrast(){
let cropperData = {} let cropperData = {}
if( cropper ){ if( cropper ){
cropperData = cropper.getData() cropperData = cropper.getData()
cropper.destroy() cropper.destroy()
} }
context.putImageData(snap,0,0) snapContext.putImageData(snap,0,0)
binary(context) binary(snapContext)
cropper = new Cropper(canvas); cropper = new Cropper(snapCanvas);
setTimeout( () => { cropper.setData(cropperData) ; },30) setTimeout( () => { cropper.setData(cropperData) ; },30)
} }
// Trigger photo take
$('.snap').on('click', function() { /*
doSnap() To convert image into binary , we will threshold it.
}); Based upon the threshold value
thresh_red, thresh_blue, thresh_green ==> are the respective red, blue and green color threshold values. Any thing above this threshold value will be denoted by white color and anything below will be black
*/
function binary(context){ function binary(context){
/*
To convert image into binary , we will threshold it.
Based upon the threshold value
thresh_red, thresh_blue, thresh_green ==> are the respective red, blue and green color threshold values. Any thing above this threshold value will be denoted by white color and anything below will be black
*/
var image = context.getImageData(0, 0, context.canvas.width, context.canvas.height); var image = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
var thresh_red = binary_level;
var thresh_green = binary_level;
var thresh_blue = binary_level;
var channels = image.data.length/4; var channels = image.data.length/4;
for(var i=0;i<channels;i++){ for(var i=0;i<channels;i++){
var r = image.data[i*4 + 0]; var r = image.data[i*4 + 0];
var g = image.data[i*4 + 1]; var g = image.data[i*4 + 1];
var b = image.data[i*4 + 2]; var b = image.data[i*4 + 2];
if( r>= thresh_red && g>= thresh_green && b>=thresh_blue ){ if( r>= binary_level ){
image.data[i*4 + 0] = 255; image.data[i*4 + 0] = 255;
image.data[i*4 + 1] = 255; image.data[i*4 + 1] = 255;
image.data[i*4 + 2] = 255; image.data[i*4 + 2] = 255;
@ -84,65 +96,28 @@ $(document).ready(function(){
context.putImageData(image, 0, 0); context.putImageData(image, 0, 0);
} }
$("#result").hide()
var slider = $( "#slider" ).slider({
range: false, function processFiles(files) {
value: 100, if(files && typeof FileReader !== "undefined") {
animate: 300, for(var i=0; i<files.length; i++) {
min : 0, fileToSnap(files[i]);
max : 255,
change: function(event,ui){
binary_level = ui.value
console.log(`binary_level:${binary_level}`)
doContrast()
} }
}); }
$("#send").on("click",function(){ }
var cropCanvas = cropper.getCroppedCanvas();
image_data = cropCanvas.toDataURL();
$("#messages").html('<div class="alert alert-primary" role="alert">Sending...</div>')
$.ajax({ var fileToSnap = function(file) {
timeout: 3000, if( (/image/i).test(file.type) ) {
url: url + "/image", var reader = new FileReader();
method:"POST", reader.onload = function(e) {
dataType: "json", let base_image = new Image();
data:{ base_image.src = e.target.result;
image_data : image_data, base_image.width = 640;
text : $("#text").val() base_image.onload = function(){
}, showSnap(base_image,640,480)
}
}).done(function(d,m,j){ };
if( d.errors ){ reader.readAsDataURL(file);
msg = d.errors.join(" / ")
$("#messages").html(`<div class="alert alert-danger" role="alert">Something went wrong: ${msg}</div>`)
return
}
$("#messages").html(`<div class="alert alert-success" role="alert">OK, your image will be available in a few seconds as <a href="#${d.hash_name}" class="hash_name btn btn-sm btn-secondary">${d.hash_name}</a></div>`)
imageList = localStorage.getItem("imageList") ? JSON.parse(localStorage.getItem("imageList")) : []
imageList.push(d.hash_name)
localStorage.setItem("imageList",JSON.stringify(imageList))
refreshImageList()
})
.fail(function(d,m,jq){
$("#messages").html(`<div class="alert alert-danger" role="alert">Ooops... Something went wrong: ${jq}</div>`)
})
})
var simuCanvas = document.getElementById("simuCanvas");
var ctx = simuCanvas.getContext("2d");
var lastpoint = { x: 0, y: 0, color: 0};
var zoom = 1;
//ctx.save
Point = function( l ){
return {
"x" : l[0],
"y" : l[1],
"color" : l[2]
} }
} }
@ -151,45 +126,44 @@ $(document).ready(function(){
pointsList = document.pointsList pointsList = document.pointsList
if (pointsList.length > 0) if (pointsList.length > 0)
{ {
ctx.clearRect(0,0,simuCanvas.width,simuCanvas.height); simuContext.clearRect(0,0,simuCanvas.width,simuCanvas.height);
lastpoint = new Point(pointsList[0]) simuLastPoint = new Point(pointsList[0])
for (var i = 0; i < pointsList.length; i+=1) for (var i = 0; i < pointsList.length; i+=1)
{ {
point = new Point(pointsList[i]) point = new Point(pointsList[i])
// if the target is black, skip drawing // if the target is black, skip drawing
if( point.color != 0){ if( point.color != 0){
ctx.beginPath() simuContext.beginPath()
ctx.shadowBlur = 5; simuContext.shadowBlur = 5;
ctx.shadowColor = 'rgba(255, 255, 255, 1)'; simuContext.shadowColor = 'rgba(255, 255, 255, 1)';
ctx.lineWidth = 2; simuContext.lineWidth = 2;
ctx.strokeStyle = "#"+(point.color + Math.pow(16, 6)).toString(16).slice(-6); simuContext.strokeStyle = "#"+(point.color + Math.pow(16, 6)).toString(16).slice(-6);
ctx.moveTo(lastpoint.x * zoom, lastpoint.y * zoom); simuContext.moveTo(simuLastPoint.x * simuZoom, simuLastPoint.y * simuZoom);
ctx.lineTo(point.x * zoom, point.y * zoom); simuContext.lineTo(point.x * simuZoom, point.y * simuZoom);
ctx.stroke(); simuContext.stroke();
ctx.closePath() simuContext.closePath()
} }
lastpoint = point simuLastPoint = point
} }
} }
} }
function refreshImageList(){ function refreshImageList(){
var imageList = localStorage.getItem("imageList") ? JSON.parse(localStorage.getItem("imageList")) : [] var imageList = localStorage.getItem("imageList") ? JSON.parse(localStorage.getItem("imageList")) : []
if( 0 == imageList.length){ if( 0 == imageList.length){
$("#preview").hide() $("#preview").hide()
return return
} }
$("#preview").show() $("#preview").show()
html = '' html = ''
for( id in imageList.reverse()){ for( id in imageList.reverse()){
hash_name = imageList[id] hash_name = imageList[id]
html += `<li class="list-group-item"><a href="#${hash_name}" class="hash_name btn-sm btn btn-secondary" rel="${hash_name}">${hash_name}</a></li>` html += `<li class="list-group-item"><a href="#${hash_name}" class="hash_name btn-sm btn btn-secondary" rel="${hash_name}">${hash_name}</a></li>`
} }
$("#imageList").html(`<ul class="list-group">${html}</ul>`) $("#imageList").html(`<ul class="list-group">${html}</ul>`)
} }
refreshImageList()
var showImage = function(e){ var showImage = function(e){
var el = $(e.target) var el = $(e.target)
@ -216,19 +190,84 @@ $(document).ready(function(){
}) })
} }
$("#imageList").on("click", showImage) // Trigger photo take
$("#messages").on("click", showImage) $('.snap').on('click', function() {
videoToSnap()
});
$( "#slider" ).slider({
range: false,
value: 100,
animate: 300,
min : 0,
max : 255,
change: function(event,ui){
binary_level = ui.value
doContrast()
}
});
$(".contrast").on("click",function(el){ $(".contrast").on("click",function(el){
let btn = el.target let btn = el.target
let val = Number($(btn).attr("rel")) let val = Number($(btn).attr("rel"))
let new_binary_level = binary_level + val; let new_binary_level = binary_level + val;
console.log(`new_binary_level:${new_binary_level}`)
if( new_binary_level >= 0 && new_binary_level <= 255){ if( new_binary_level >= 0 && new_binary_level <= 255){
binary_level = new_binary_level binary_level = new_binary_level
$("#slider").slider("option","value",binary_level) $("#slider").slider("option","value",binary_level)
} }
}) })
$("#imageList").on("click", showImage)
$("#messages").on("click", showImage)
$("#send").on("click",function(){
var cropCanvas = cropper.getCroppedCanvas();
image_data = cropCanvas.toDataURL();
$("#messages").html('<div class="alert alert-primary" role="alert">Sending...</div>')
$.ajax({
timeout: 3000,
url: url + "/image",
method:"POST",
dataType: "json",
data:{
image_data : image_data,
text : $("#text").val()
},
}).done(function(d,m,j){
if( d.errors ){
msg = d.errors.join(" / ")
$("#messages").html(`<div class="alert alert-danger" role="alert">Something went wrong: ${msg}</div>`)
return
}
$("#messages").html(`<div class="alert alert-success" role="alert">OK, your image will be available in a few seconds as <a href="#${d.hash_name}" class="hash_name btn btn-sm btn-secondary" rel="${d.hash_name}">${d.hash_name}</a></div>`)
imageList = localStorage.getItem("imageList") ? JSON.parse(localStorage.getItem("imageList")) : []
imageList.push(d.hash_name)
localStorage.setItem("imageList",JSON.stringify(imageList))
refreshImageList()
})
.fail(function(d,m,jq){
$("#messages").html(`<div class="alert alert-danger" role="alert">Ooops... Something went wrong: ${jq}</div>`)
})
})
$('#uploadBtn').on('click', function(e) {
e.stopPropagation();
e.preventDefault();
//trigger default file upload button
uploadInput.click();
});
$('#uploadInput').on('change', function() {
//retrieve selected uploaded files data
var files = $(this)[0].files;
processFiles(files);
return false;
});
refreshImageList()
}) })

3
server/.env.template Normal file
View File

@ -0,0 +1,3 @@
DB_HOST="127.0.0.1"
DB_PORT="6379"
DEBUG=1

View File

@ -16,13 +16,13 @@ services:
app: app:
env_file: .env env_file: .env
build: . build: ../
image: teamlaser/laser-app:latest image: teamlaser/laser-app:latest
ports: ports:
- "5000:5000" - "5000:5000"
depends_on: depends_on:
- db - db
command: python ./server.py command: python /opt/server.py
assets: assets:
env_file: .env env_file: .env
@ -35,9 +35,8 @@ services:
worker: worker:
env_file: .env env_file: .env
build: .
image: teamlaser/laser-app:latest image: teamlaser/laser-app:latest
depends_on: depends_on:
- db - db
command: bash -c "while true; do python ./worker.py; sleep 3; done" command: bash -c "while true; do python /opt/worker.py; sleep 3; done"

View File

@ -11,7 +11,7 @@ rm -f /etc/nginx/sites-enabled/*
pip3 install flask numpy pillow redis pip3 install flask numpy pillow redis
pip3 install pypotrace pip3 install pypotrace
cd /opt cd /opt
#git clone https://git.interhacker.space/teamlaser/laser-app git clone https://git.interhacker.space/teamlaser/laser-app
cp /opt/teamlaser/laser-app/files/nginx/sites-enabled/site.conf /etc/nginx/sites-enabled cp /opt/laser-app/server/files/nginx/sites-enabled/site.conf /etc/nginx/sites-enabled