diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..25c8fdbab --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +package-lock.json \ No newline at end of file diff --git a/README.md b/README.md index 88765ffde..066a7654b 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,25 @@ -# Final Project -*Due before the start of class, October 11th (final day of the term)* - -For your final project, you'll implement a web application that exhibits understanding of the course materials. -This project should provide an opportunity to both be creative and to pursue individual research and learning goals. - -## General description -Your project should consist of a complete Web application, exhibiting facets of the three main sections of the course material: - -- Static Web page content and design. You should have a project that is accessible, easily navigable, and features significant content. -- Dynamic behavior implemented with JavaScript (TypeScript is also allowed if your group wants to explore it). -- Server-side programming *using Node.js*. Typically this will take the form of some sort of persistent data (database), authentication, and possibly server-side computation. -- A video (less than five minutes) where each group member explains some aspect of the project. An easy way to produce this video is for you all the groups members to join a Zoom call that is recorded; each member can share their screen when they discuss the project or one member can "drive" the interface while other members narrate (this second option will probably work better.) The video should be posted on YouTube or some other accessible video hosting service. Make sure your video is less than five minutes, but long enough to successfully explain your project and show it in action. There is no minimum video length. - -## Project ideation -Excellent projects typically serve someone/some group; for this assignment you need to define your users and stakeholders. I encourage you to identify projects that will have impact, either artistically, politically, or in terms of productivity. - -## Logistics -### Team size -Students are will work in teams of 3-5 students for the project; teams of two can be approved with the permission of the instructor. Working in teams should help enable you to build a good project in a limited amount of time. Use the `#project-logistics` channel in Discord to pitch ideas for final projects and/or find fellow team members as needed. - -Teams must be in place by end of day on Saturday, September 25th. If you have not identified a team at this point, you will be assigned a team. You will be given some class time on Monday to work on your proposal, but please plan on reserving additional time as needed. - -### Deliverables - -__Proposal:__ -Provide an outline of your project direction and the names of associated team members. -The outline should have enough detail so that staff can determine if it meets the minimum expectations, or if it goes too far to be reasonable by the deadline. Please include a general description of a project, and list of key technologies/libraries you plan on using (e.g. React, Three.js, Svelte, TypeScript etc.). Name the file proposal.md and submit a pull request. -Submit a PR to turn it in by Monday, September 27th at11:59 PM. Only one pull request is required per team. - -There are no other scheduled checkpoints for your project. - -#### Turning in Your Outline / Project -Submit a second PR on the final project repo to turn in your app and code. Again, only one pull request per team. - -Deploy your app, in the form of a webpage, to Glitch/Heroku/Digital Ocean or some other service; it is critical that the application functions correctly wherever you post it. - -The README for your second pull request doesn’t need to be a formal report, but it should contain: - -1. A brief description of what you created, and a link to the project itself (two paragraphs of text) -2. Any additional instructions that might be needed to fully use your project (login information etc.) -3. An outline of the technologies you used and how you used them. -4. What challenges you faced in completing the project. -5. What each group member was responsible for designing / developing. -6. A link to your project video. - -Think of 1,3, and 4 in particular in a similar vein to the design / tech achievements for A1—A4… make a case for why what you did was challenging and why your implementation deserves a grade of 100%. - -## FAQs - -- **Can I use XYZ framework?** You can use any web-based frameworks or tools available, but for your server programming you need to use Node.js. Your client-side scripting language should be either JavaScript or TypeScript. +### CS4241 Final Project + +Group #: 21 + +Group Members: Aditya Kumar, Matthew Malone, William White + +1. This project is a simple photo editor using Javascript. The user can upload an image, and then manipulate it. The user can crop, rotate, and transform the image. Various filter and color manipulation options are also offered. We created a custom function that allows a user to "Jpegify" an image and introduce artifacts in order to distort it. The website can be found hosted on glitch at [Meme Machine](https://group21-meme-machine.glitch.me). +2. Simply click the upload button and upload an image of reasonable size. The program will inform you if your image is too large. Note that because this uses the processing power of the client's computer, depending on the specs large images may cause lag or crashes, hence the automatic limiting of image sizes. +3. We used three different libraries to do the bulk of the work in the project. We also used a CSS library to do the styling. + 1. Canvas: We used canvas to display the image and make real time edits to it. This was difficult to implement as we had to make it dynamically update with any changes made to the photo, and keep it working with all the libraries. + 2. Cropper-JS: CropperJS was chosen as a cropping tool library because of its integration to canvas. This allows the user to crop out sections of an image, rotate, and transform images. This was difficult to get working because we needed to get it imported to the client somehow. We wound up having to use Helmet-CSP to get all our scripts working properly. We also had to dynamically program all the controls to enable/disable themselves as necessary. + 3. Caman: Caman is a third party library used to manipulate photos. It was chosen because of its easy integration to canvas. It was used to make all the sliders for editing brightness, saturation, etc. It was also used to apply several filters to the image. This was challenging to implement simply because the documentation wasn't that great, so we had to do a lot of experimentation to get it to work properly. It was implemented by use of buttons and sliders. + 4. We used basic javascript photo manipulation to add a button that repeatedly converts an image into a jpeg to introduce artifacts. This can be used to distort any image and can be found by clicking the Compress button. + 5. Bootstrap was used to design the layout and styling of the websites. It was used to make buttons look nicer, and we also used it to style sliders and div containers. + 6. Helmet-CSP was used to set content security policy headers to allow inline scripts to be included from Javascript CDNs. +4. Challenges: + 1. The first challenge we found was getting the scripts to the client. We found out that we needed to set content security policy headers to get the Caman and CropperJS libraries included. We wound up using Helmet-CSP to set content security policy headers for the project. + 2. The second challenge we faced was getting the libraries to cooperate with canvas. Cropper for example is good for making edits to a photo file, but we needed to write code to dynamically update the canvas to reflect changes to the image data. + 3. Bootstrap was a completely new experience for us, so we needed to learn how to use it from scratch for this project. + 4. The fourth challenge we encountered was that we needed a concise way to implement multiple listeners for the Caman JS sliders and make them apply in combination to each other. This was accomplished by writing one listener and applying it to the entire array of sliders. +5. What each team member was responsible for: + 1. Matthew Malone: Responsible for server backend, cropperJS implementation, and canvas setup. Creation of HTML controls for CropperJS. + 2. Aditya Kumar: Responsible for Caman setup and sliders/filters. Video creation and editing. Creation of HTML controls for CamanJS. + 3. Will White: Responsible for all styling of webpages. Learned and used bootstrap to accomplish styling of the editor. +6. [Project Video](https://drive.google.com/file/d/1e4F3sxtnmZ6tdkjJp6dVW4bQ-zrbjwna/view?usp=sharing) diff --git a/package.json b/package.json new file mode 100644 index 000000000..2e2c614ad --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "a4-final-project", + "version": "1.0.0", + "description": "Submission for Group 21", + "author": "God", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "repository": "https://github.com/hellofellowkids/final_project", + "dependencies": { + "express": "^4.17.1", + "express-validator": "^6.12.1", + "morgan": "^1.10.0", + "cors": "^2.8.5", + "browserify": "^17.0.0", + "cropperjs": "^1.5.12", + "helmet-csp": "^3.4.0", + "caman": "^4.1.2" + }, + "engines": { + "node": "14.x" + }, + "license": "NONE" +} diff --git a/pictures/mock_up.png b/pictures/mock_up.png new file mode 100644 index 000000000..3c108032f Binary files /dev/null and b/pictures/mock_up.png differ diff --git a/proposal.md b/proposal.md new file mode 100644 index 000000000..c7bb0e803 --- /dev/null +++ b/proposal.md @@ -0,0 +1,36 @@ +CS4241 Final Project Proposal +--- + +Group #: 21
+Group Members: Aditya Kumar, Matthew Malone, William White + +## Project Idea + +We want to create a website that will host a simple photo manipulation / editor tool. While there are websites and software that manipulate photos, we find a majority of them to be too intricate or pricey to use. We believe that we can deliver a photo manipulation website that is accessible and simple to use. +We will use primarily Javascript for this project. + +Some simple photo manipulation features we'd like to include (given the deadline): +- Apply Filters +- Add Text +- Crop / Morph + +If more time allows, we can add more features to make the website more robust. + +## Libraries we can use to accomplish this: +- Pica --Resize images +- Lena.js --Image processing +- Jimp --Another image processing library written entirely in JS +- Grade.js --Make gradient from supplied image. +- Compressor.js --Javascript image compressor + +## General Logic Flow for App + +1) Client uploads a photo to edit via file select prompt +2) Website displays uploaded photo and toolbar widget +3) Client interacts with toolbar widget to manipulate the photo. +4) For each manipulation, the photo display will be updated to show a preview of the final product +5) Client is satisfied with manipulated photo, they click an "Export" button to download the final manipulated photo. + +## Initial Website Mock-up + +![image info](./pictures/mock_up.png) \ No newline at end of file diff --git a/public/img/favicon.ico b/public/img/favicon.ico new file mode 100644 index 000000000..4c4ae239d Binary files /dev/null and b/public/img/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 000000000..e2776b4fa --- /dev/null +++ b/public/index.html @@ -0,0 +1,95 @@ + + + + + + + + + + Meme Machine + + + +
+ + + +
+ Your browser does not support canvas. +
+
+ +
+
+ +
+ + +
+ + +
+ +
+ + + +
+
+ + +
+
+ + +
+ +
+
+ + + + + +
+
+ + + + + +
+
+ +
+
+ + +
+
+ +
+
+ + + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/public/scripts/editor.js b/public/scripts/editor.js new file mode 100644 index 000000000..7225f9a5a --- /dev/null +++ b/public/scripts/editor.js @@ -0,0 +1,391 @@ +// Retrieve documents elements +let imgBtn = document.getElementById('imageInput') +const download = document.getElementById('download') +let canvas = document.getElementById('imgCanvas') +const topTextBtn = document.getElementById('topTextBtn') +const bottomTextBtn = document.getElementById('bottomTextBtn') +const startCropBtn = document.getElementById('startCropBtn') +const cropBtn = document.getElementById('cropBtn') +const restoreCropBtn = document.getElementById('restoreCropBtn') +const rotateLeftBtn = document.getElementById('rotateLeft') +const rotateRightBtn = document.getElementById('rotateRight') +const flipXBtn = document.getElementById('flipX') +const flipYBtn = document.getElementById('flipY') +const compressBtn = document.getElementById('compressBtn') +const greyscaleBtn = document.getElementById('greyscaleBtn') +const resetBtn = document.getElementById('resetButton') +const embossBtn = document.getElementById('embossBtn') +const herMajestyBtn = document.getElementById('herMajestyBtn') +const orangePeelBtn = document.getElementById('orangePeelBtn') +const sinCityBtn = document.getElementById('sinCityBtn') + +// slider elements +const brightnessSlider = document.getElementById('brightnessSlider') +const saturationSlider = document.getElementById('saturationSlider') +const vibranceSlider = document.getElementById('vibranceSlider') +const exposureSlider = document.getElementById('exposureSlider') +const hueSlider = document.getElementById('hueSlider') +const sepiaSlider = document.getElementById('sepiaSlider') +const gammaSlider = document.getElementById('gammaSlider') +const noiseSlider = document.getElementById('noiseSlider') +const clipSlider = document.getElementById('clipSlider') +const sharpenSlider = document.getElementById('sharpenSlider') + +// additional variables +let imgData +let context +let userImage +let cropper +document.querySelectorAll('.range-field').forEach(slider => { + slider.addEventListener('change', function() { + let brightnessValue = brightnessSlider.value + let saturationValue = saturationSlider.value + let vibranceValue = vibranceSlider.value + let exposureValue = exposureSlider.value + let hueValue = hueSlider.value + let sepiaValue = sepiaSlider.value + let gammaValue = gammaSlider.value + let noiseValue = noiseSlider.value + let clipValue = clipSlider.value + let sharpenValue = sharpenSlider.value + Caman(canvas, function () { + this.revert(false) + this.brightness(brightnessValue) + this.saturation(saturationValue) + this.vibrance(vibranceValue) + this.exposure(exposureValue) + this.hue(hueValue) + this.sepia(sepiaValue) + this.gamma(gammaValue) + this.noise(noiseValue) + this.clip(clipValue) + this.sharpen(sharpenValue) + this.render() + }) + }) +}) + +// Sliders +/*brightnessSlider.addEventListener('change', function(){ + let brightnessValue = brightnessSlider.value + let saturationValue = saturationSlider.value + let vibranceValue = vibranceSlider.value + let exposureValue = exposureSlider.value + let hueValue = hueSlider.value + let sepiaValue = sepiaSlider.value + let gammaValue = gammaSlider.value + let noiseValue = noiseSlider.value + let clipValue = clipSlider.value + let sharpenValue = sharpenSlider.value + let stackBlurValue = stackBlurSlider.value + Caman(canvas, function () { + this.revert(false) + this.brightness(brightnessValue) + this.saturation(saturationValue) + this.vibrance(vibranceValue) + this.render() + }) +}) + +saturationSlider.addEventListener('change', function(){ + let value = saturationSlider.value + Caman(canvas, function () { + this.revert(false) + this.saturation(value).render(); + }) +}) + +vibranceSlider.addEventListener('change', function(){ + let value = vibranceSlider.value + Caman(canvas, function () { + this.vibrance(value).render(); + }) +}) + +exposureSlider.addEventListener('change', function(){ + let value = exposureSlider.value + Caman(canvas, function () { + this.exposure(value).render(); + }) +}) + +hueSlider.addEventListener('change', function(){ + let value = hueSlider.value + Caman(canvas, function () { + this.hue(value).render(); + }) +}) + +sepiaSlider.addEventListener('change', function(){ + let value = sepiaSlider.value + Caman(canvas, function () { + this.sepia(value).render(); + }) +}) + +gammaSlider.addEventListener('change', function(){ + let value = gammaSlider.value + Caman(canvas, function () { + this.gamma(value).render(); + }) +}) + + +noiseSlider.addEventListener('change', function(){ + let value = noiseSlider.value + Caman(canvas, function () { + this.noise(value).render(); + }) +}) + +clipSlider.addEventListener('change', function(){ + let value = clipSlider.value + Caman(canvas, function () { + this.clip(value).render(); + }) +}) + +sharpenSlider.addEventListener('change', function(){ + let value = sharpenSlider.value + Caman(canvas, function () { + this.sharpen(value).render(); + }) +}) + +stackBlurSlider.addEventListener('change', function(){ + let value = stackBlurSlider.value + Caman(canvas, function () { + this.stackBlur(value).render(); + }) +})*/ + +// Reset button +resetBtn.addEventListener('click', function() { + location.reload(true) +}) + +// Sin City button +sinCityBtn.addEventListener('click', function(){ + sinCityBtn.innerText = "Rendering..." + Caman(canvas, function () { + this.sinCity().render() + sinCityBtn.innerText = "Sin City" + }) + +}) + +// Orange Peel button +orangePeelBtn.addEventListener('click', function(){ + orangePeelBtn.innerText = "Rendering..." + Caman(canvas, function () { + this.orangePeel().render() + orangePeelBtn.innerText = "Orange Peel" + }) +}) + +// Her Majesty button +herMajestyBtn.addEventListener('click', function(){ + herMajestyBtn.innerText = "Rendering..." + Caman(canvas, function () { + this.herMajesty().render() + herMajestyBtn.innerText = "Her Majesty" + }) + +}) + +// Emboss button +embossBtn.addEventListener('click', function(){ + embossBtn.innerText = "Rendering..." + Caman(canvas, function () { + this.emboss().render() + embossBtn.innerText = "Emboss" + }) + +}) + +// Greyscale button +greyscaleBtn.addEventListener('click', function(){ + greyscaleBtn.innerText = "Rendering..." + Caman(canvas, function () { + this.greyscale().render() + greyscaleBtn.innerText = "Greyscale" + }) + +}) + + +// Compress button +compressBtn.addEventListener('click', function(){ + for(let i = 0; i < 20; i++) { + let tempImage = new Image() + tempImage.src = canvas.toDataURL("image/jpeg", 0.05) + context.drawImage(tempImage, 0, 0) + } +}) + +// Flip buttons +flipXBtn.addEventListener('click',function() { + if(cropper.getData().scaleX === 1) + cropper.scaleX(-1) + else + cropper.scaleX(1) +}) +flipYBtn.addEventListener('click',function() { + if(cropper.getData().scaleY === 1) + cropper.scaleY(-1) + else + cropper.scaleY(1) +}) + +// Rotation buttons +rotateRightBtn.addEventListener('click', function() { + cropper.rotate(45) +}) +rotateLeftBtn.addEventListener('click', function() { + cropper.rotate(-45) +}) + +// Related to cropped +startCropBtn.addEventListener('click', function() { + startCropBtn.disabled = true + cropBtn.disabled = false + restoreCropBtn.disabled = false + rotateLeftBtn.disabled = false + rotateRightBtn.disabled = false + flipXBtn.disabled = false + flipYBtn.disabled = false + cropper = new Cropper(canvas,{crop(event) { + console.log(event.detail.x); + console.log(event.detail.y); + console.log(event.detail.width); + console.log(event.detail.height); + console.log(event.detail.rotate); + console.log(event.detail.scaleX); + console.log(event.detail.scaleY); + },viewMode: 2, dragMode: 'crop',}) +}) +cropBtn.addEventListener('click', function() { + const image = cropper.getCroppedCanvas() + /*const link = document.createElement('a') + link.download = 'download.png' + link.href = image + link.click() + link.delete()*/ + cropper.destroy() + context = canvas.getContext('2d') + canvas.height = image.height + canvas.width = image.width + context.drawImage(image,0,0) + startCropBtn.disabled = false + cropBtn.disabled = true + restoreCropBtn.disabled = true + rotateLeftBtn.disabled = true + rotateRightBtn.disabled = true + flipXBtn.disabled = true + flipYBtn.disabled = true +}) +restoreCropBtn.addEventListener('click', function() { + cropper.destroy() + context = canvas.getContext('2d') + canvas.width = userImage.width + canvas.height = userImage.height + context.drawImage(userImage,0,0) + + imgData = canvas.toDataURL("image/png",1.0) + startCropBtn.disabled = false + cropBtn.disabled = true + restoreCropBtn.disabled = true + rotateLeftBtn.disabled = true + rotateRightBtn.disabled = true + flipXBtn.disabled = true + flipYBtn.disabled = true +}) + +// Add top-text button +topTextBtn.addEventListener('click', function() { + if(document.getElementById('topTextSize').value !== "" || document.getElementById('topTextSize') !== null) + context.font = `${document.getElementById('topTextSize').value}pt Impact` + else + context.font = "36pt Impact"; + context.fillStyle = "white" + context.strokeStyle = "black" + context.textAlign = "center" + const text = document.getElementById('topText').value + wrapText(text,canvas.width/2,canvas.height/8,canvas.width,50) +}) + +// Add bottom-text button +bottomTextBtn.addEventListener('click', function() { + if(document.getElementById('bottomTextSize').value !== "" || document.getElementById('bottomTextSize') !== null) + context.font = `${document.getElementById('bottomTextSize').value}pt Impact` + else + context.font = "36pt Impact"; + context.fillStyle = "white" + context.strokeStyle = "black" + context.textAlign = "center" + const text = document.getElementById('bottomText').value + wrapText(text,canvas.width/2,canvas.height-(canvas.height/12),canvas.width,50) +}) + +// Wrap function +function wrapText(text, x, y, maxWidth, lineHeight) { + const words = text.split(' '); + let line = ''; + + for(let n = 0; n < words.length; n++) { + const testLine = line + words[n] + ' ' + const metrics = context.measureText(testLine) + const testWidth = metrics.width; + if (testWidth > maxWidth && n > 0) { + context.fillText(line, x, y) + context.strokeText(line, x, y) + line = words[n] + ' ' + y += lineHeight + } + else { + line = testLine + } + } + context.fillText(line, x, y) + context.strokeText(line,x,y) +} + +// Download button +download.addEventListener('click', function() { + const link = document.createElement('a') + link.download = 'download.png' + link.href = canvas.toDataURL() + link.click() + link.delete() +}); + +// image button +imgBtn.addEventListener('change', function(event) { + if(event.target.files) {//If files + let imageFile = event.target.files[0] + const reader = new FileReader(); + reader.readAsDataURL(imageFile) + reader.onloadend = function(event) { + userImage = new Image() + // noinspection JSValidateTypes + userImage.src = event.target.result + userImage.onload = function () { + context = canvas.getContext('2d') + const MAX_WIDTH = 1152 + const MAX_HEIGHT = 1000 + if(userImage.width > MAX_WIDTH || userImage.height > MAX_HEIGHT){ + alert("The uploaded image is too large to handle. " + + `Please upload an image that will fit in ${MAX_WIDTH} x ${MAX_HEIGHT}. \n \n` + + `For reference, your image was ${userImage.width} x ${userImage.height}`) + } + else { + canvas.width = userImage.width + canvas.height = userImage.height + context.drawImage(userImage,0,0) + startCropBtn.disabled = false + imgData = canvas.toDataURL("image/png",1.0) + } + } + } + } +}) \ No newline at end of file diff --git a/public/style.css b/public/style.css new file mode 100644 index 000000000..91677f088 --- /dev/null +++ b/public/style.css @@ -0,0 +1,148 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 2em 1em; + background-color: rgb(237, 250, 252); +} + +h1 { + line-height: 1.1; +} + +input { + border: 1px solid silver; + font-size: 16px; + padding: 5px; +} + +div[class="editor"] { + margin-bottom: 5px; + margin-left: 15px; +} + +label[class="sectionLabel"] { + display: block; + text-align: center; + font-weight: bold; + font-size: large; + padding: 5px; +} + +div[class="controlHolder"] { + border: 3px solid rgb(180, 187, 189); + border-radius: 25px; + background-color: rgb(192, 209, 212); + width: 500px; + flex-shrink: 0; + padding: 10px; + position: relative; + +} + +div[class="textInputs"] { + padding: 10px; + text-align: center; + width: 100%; +} + +input [type="text"], [class="pull-left"]{ + display: inline-block; + text-align: center; + width: 100%; +} + +label[class="topInputs"] { + padding: 10px; + display: inline-block; + text-align: center; + width: 100%; +} + +label[class="bottomInputs"] { + padding: 10px; + display: inline-block; + text-align: center; + width: 100%; +} + +div[class="cropControls"] { + padding: 10px; + text-align: center; + width: 100%; +} + +div[class="rotators"] { + padding: 10px; + text-align: center; + width: 100%; +} + +div[class="flippers"] { + padding: 10px; + text-align: center; + width: 100%; +} + +div[class="sliderGroup"] { + border: 2px solid rgb(178, 184, 185); + border-radius: 5px; + background-color: rgb(185, 201, 204); + width: 100%; + flex-shrink: 0; + padding: 5px; + display: flex; +} + +div[class="sliderGroupLeft"] { + text-align: left; + margin-left: 15px; +} + +div[class="sliderGroupRight"] { + text-align: left; +} + +label[class="sliderLabel"] { + min-width: 4.5em +} + +input[type='range'], +br { + -webkit-appearance: none; + background-color: #ecf0f1; + border: 1px solid #bdc3c7; + width: 200px; + height: 10px; + border-radius: 10px; + vertical-align: middle; + line-height: 1.6 +} + +input[type=range]::-webkit-slider-thumb { + border: 1px solid #363636; + height: 20px; + width: 15px; + border-radius: 5px; + background: #535252; + cursor: pointer; + -webkit-appearance: none; +} + +div[class="filtersHolder"] { + border: 2px solid rgb(178, 184, 185); + border-radius: 5px; + background-color: rgb(185, 201, 204); + width: 100%; + flex-shrink: 0; + padding: 10px; + text-align: center; +} + +div[class="filterSubgroup"] { + padding: 10px; + text-align: center; + width: 100%; +} \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 000000000..53498a5b7 --- /dev/null +++ b/server.js @@ -0,0 +1,30 @@ +const express = require('express') +const morgan = require('morgan') +const cors = require('cors') +const csp = require('helmet-csp') +const app = express(); +app.use(express.static('public')) +app.use(express.static(__dirname)); +app.use(express.urlencoded({ extended: true })); +app.use(express.json()); +app.use(morgan('tiny')) +app.use(cors()) +app.use(csp({ + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-eval'", "'unsafe-inline'", 'https://cdn.jsdelivr.net/npm/cropperjs', 'https://cdn.jsdelivr.net/npm/caman'] + } +})) + + +app.use( function( req, res, next ) { + console.log( 'url:', req.url ) + next() +}) +app.get( '/', function (req, res) { + res.sendFile(__dirname + "/public/index.html" ) +}) + +app.listen(3000, () => { + console.log(`Example app listening at http://localhost:${3000}`) +}) \ No newline at end of file