main.js (11248B)
1 let canvas; 2 let video; 3 let ctx; 4 /* 5 * theses settings correspond to a css-filter. you can specify an optional 6 * filter attribute to modify the value from the input element. 7 */ 8 let settings = { 9 "brightness": {}, 10 "saturate": {}, 11 "contrast": {}, 12 "hue-rotate": { filter: (value) => value + "deg" }, 13 "grayscale": {}, 14 "sepia": {}, 15 "invert": {}, 16 "blur": { filter: (value) => (value * canvas.width) / 100 + "px" }, 17 }; 18 const img = new Image(); 19 let lensflare_active = false; 20 let cursor = { 21 x: 0, 22 y: 0, 23 }; 24 25 // wait for site to be parsed so element can be found 26 document.addEventListener("DOMContentLoaded", function () { 27 canvas = document.getElementById("myCanvas"); 28 ctx = canvas.getContext("2d"); 29 video = document.getElementById("video"); 30 document 31 .getElementById("back") 32 .addEventListener( 33 "click", 34 () => (document.body.className = "import-active"), 35 ); 36 37 // bind listeners 38 document 39 .getElementById("viewport") 40 .addEventListener("drop", drop_handler); 41 document 42 .getElementById("viewport") 43 .addEventListener("dragover", (event) => event.preventDefault()); 44 document 45 .getElementById("take-picture") 46 .addEventListener("click", use_camera); 47 document 48 .getElementById("cheese") 49 .addEventListener("click", take_picture); 50 document 51 .getElementById("upload-image") 52 .addEventListener("change", upload_image); 53 54 for (let element of document.getElementsByClassName("save-image")) { 55 element.addEventListener("click", save_image); 56 } 57 for (let element of document.getElementsByClassName("share-image")) { 58 element.addEventListener("click", share_image); 59 } 60 61 for (let element of document.getElementsByClassName("lensflare")) { 62 element.addEventListener("click", function (event) { 63 event.preventDefault(); 64 lensflare_active = !lensflare_active; 65 if (lensflare_active) { 66 event.target.classList.add("is-primary"); 67 } else { 68 event.target.classList.remove("is-primary"); 69 } 70 draw(true); 71 }); 72 } 73 74 document 75 .getElementById("viewport") 76 .addEventListener("mousemove", function (event) { 77 if (event.buttons === 1 && lensflare_active) { 78 cursor.x = 79 (event.clientX - canvas.offsetLeft) / canvas.clientWidth - 0.5; 80 cursor.y = 81 (event.clientY - canvas.offsetTop) / canvas.clientWidth - 0.5; 82 console.log(cursor); 83 draw(true); 84 } 85 }); 86 87 window.addEventListener("deviceorientation", function (event) { 88 if (lensflare_active) { 89 cursor.x = (event.gamma / 360) * 4; 90 cursor.y = ((event.beta - 90) / 360) * 4; 91 console.log(cursor); 92 // draw(true); 93 } 94 }); 95 96 function animation() { 97 if (lensflare_active) { 98 draw(true); 99 } 100 requestAnimationFrame(animation); 101 } 102 animation(); 103 104 window.addEventListener("resize", () => draw(true)); 105 106 video.addEventListener("canplay", function () { 107 const width = 320; 108 let height = video.videoHeight / (video.videoWidth / width); 109 110 // Firefox currently has a bug where the height can't be read from 111 // the video, so we will make assumptions if this happens. 112 113 if (isNaN(height)) { 114 height = width / (4 / 3); 115 } 116 video.width = width; 117 video.height = height; 118 }); 119 120 for (let setting in settings) { 121 // make an array out of an iterable 122 const elements = [...document.getElementsByClassName(setting)]; 123 settings[setting].elements = elements; 124 // if filter is not definded, use identity function 125 settings[setting].filter ||= (value) => value; 126 for (let element of elements) { 127 element.addEventListener("input", settings_apply); 128 } 129 130 const reset_elements = document.getElementsByClassName(setting + "-reset"); 131 for (let reset_element of reset_elements) { 132 reset_element.addEventListener("click", () => reset_all(setting)); 133 } 134 } 135 }); 136 137 /** 138 * Reset all inputs of a setting back to the default value 139 * 140 * @param {string} setting - key of the settings object to reset 141 */ 142 function reset_all(setting) { 143 console.log("reseting " + setting); 144 for (let element of settings[setting].elements) { 145 element.value = element.defaultValue; 146 } 147 draw(true); 148 } 149 150 /** 151 * Request camera access and on succhess, show camera feed on video-element and 152 * switch to camera-active view 153 */ 154 function use_camera() { 155 navigator.mediaDevices 156 .getUserMedia({ video: true, audio: false }) 157 .then((stream) => { 158 video.srcObject = stream; 159 video.play(); 160 document.body.className = "camera-active"; 161 }) 162 .catch((err) => { 163 console.error(`An error occurred: ${err}`); 164 }); 165 } 166 167 /** 168 * Take a still frame of the video-element showing the camera-feed and load it 169 * to the img to be rendered by canvas and switch to editor-active view 170 */ 171 function take_picture() { 172 canvas.width = video.width; 173 canvas.height = video.height; 174 ctx.drawImage(video, 0, 0, canvas.width, canvas.height); 175 img.src = canvas.toDataURL("image/png"); 176 img.onload = () => draw(true); 177 178 document.body.className = "editor-active"; 179 } 180 181 /** 182 * Load selected file by input element to img to be rendered by canvas and 183 * switch to editor-active view 184 */ 185 function upload_image() { 186 document.body.className = "editor-active"; 187 188 console.log(this.files[0]); 189 190 img.src = URL.createObjectURL(this.files[0]); 191 img.onload = () => draw(true); 192 } 193 194 /** 195 * Get file that is dropped into the website and load it to img to be rendered 196 * by canvas and switch to editor-active view. 197 * 198 * @param {DragEvent} ev -supplier by event listener 199 * 200 * https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop 201 */ 202 function drop_handler(ev) { 203 ev.preventDefault(); 204 let file; 205 206 if (ev.dataTransfer.items) { 207 const item = ev.dataTransfer.items[0]; 208 if (item.kind !== "file") return; 209 file = item.getAsFile(); 210 } else { 211 file = ev.dataTransfer.files[0]; 212 } 213 console.log(file); 214 215 document.body.className = "editor-active"; 216 img.src = URL.createObjectURL(file); 217 img.onload = () => draw(true); 218 } 219 220 /** 221 * Creates a download of the edited image in full resolution by creating a link 222 * and virtually clicking it. 223 * 224 * @param {PointerEvent} event - supplied by event listener 225 */ 226 function save_image(event) { 227 event.preventDefault(); 228 draw(false); 229 230 const dataUrl = canvas.toDataURL("image/png"); 231 // downloading only works with links but not window.open 232 const link = document.createElement("a"); 233 link.href = dataUrl; 234 link.download = "imagine.png"; 235 link.click(); 236 } 237 238 /** 239 * Uses the navigator.share API to share the edited image in full reslution. 240 * 241 * @param {PointerEvent} event - supplied by event listener 242 */ 243 function share_image(event) { 244 event.preventDefault(); 245 if (!navigator.share) { 246 console.log("navigator.share does not exist"); 247 return; 248 } 249 250 canvas.toBlob(async (blob) => { 251 if (!blob) return; 252 const file = new File([blob], "imagine.png", { type: "image/png" }); 253 254 try { 255 await navigator.share({ files: [file] }); 256 } catch (error) { 257 console.log("Error sharing:", error); 258 } 259 }, "image/png"); 260 } 261 262 /** 263 * Set all inputs of a setting to the value of the input that changed it. 264 * 265 * @param {Event} event - supplied by event listener 266 */ 267 function settings_apply(event) { 268 const changed_setting = event.target.id; 269 const new_value = event.target.value; 270 271 // update all inputs for that setting (mobile and desktop) to the new value 272 for (let element of settings[changed_setting].elements) { 273 if (element == event.target) continue; 274 element.value = new_value; 275 } 276 277 draw(true); 278 } 279 280 /** 281 * Render a lensflare shader for every pixel on the canvas. 282 * 283 * @param {number} pos_x - x position of the lensflare in the range [-0.5,0.5] 284 * @param {number} pos_y - y position of the lensflare in the range [-0.5,0.5] 285 * https://www.shadertoy.com/view/ldSXWK 286 */ 287 function lensflare(pos_x, pos_y) { 288 const imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height); 289 const pixel_count = imgdata.data.length / 4; 290 const aspect_ratio = canvas.width / canvas.height; 291 for (let i = 0; i < pixel_count; i++) { 292 const x = i % canvas.width; 293 const y = i / canvas.width; 294 const u = (x / canvas.width - 0.5) * aspect_ratio; 295 const v = y / canvas.height - 0.5; 296 297 const intensity = 4 * 255; 298 const uv_len = u * u + v * v; 299 const uvd_x = u * uv_len; 300 const uvd_y = v * uv_len; 301 const uvd_len = uvd_x * uvd_x + uvd_y * uvd_y; 302 303 const uvd_pos_x = uvd_x + pos_x; 304 const uvd_pos_y = uvd_y + pos_y; 305 let temp = uvd_pos_x * uvd_pos_x + uvd_pos_y * uvd_pos_y; 306 const f2 = Math.max(1.0 / (1.0 + 32.0 * temp * 0.64), 0.0) * 0.1; 307 const f22 = Math.max(1.0 / (1.0 + 32.0 * temp * 0.72), 0.0) * 0.08; 308 const f23 = Math.max(1.0 / (1.0 + 32.0 * temp * 0.81), 0.0) * 0.06; 309 310 let uvx_x = u * (1 + 0.5) + uvd_x * 0.5; 311 let uvx_y = v * (1 + 0.5) + uvd_y * 0.5; 312 let uvx_pos_x = uvx_x + pos_x; 313 let uvx_pos_y = uvx_y + pos_y; 314 315 temp = uvx_pos_x * uvx_pos_x + uvx_pos_y * uvx_pos_y; 316 const f4 = Math.max(0.01 - temp * 0.16, 0.0) * 6.0; 317 const f42 = Math.max(0.01 - temp * 0.2, 0.0) * 5.0; 318 const f43 = Math.max(0.01 - temp * 0.25, 0.0) * 3.0; 319 320 uvx_x = u * (1 + 0.4) + uvd_x * 0.4; 321 uvx_y = v * (1 + 0.4) + uvd_y * 0.4; 322 uvx_pos_x = uvx_x + pos_x; 323 uvx_pos_y = uvx_y + pos_y; 324 325 temp = uvx_pos_x * uvx_pos_x + uvx_pos_y * uvx_pos_y; 326 const f5 = Math.max(0.01 - temp * 0.04, 0.0) * 2.0; 327 const f52 = Math.max(0.01 - temp * 0.16, 0.0) * 2.0; 328 const f53 = Math.max(0.01 - temp * 0.36, 0.0) * 2.0; 329 330 uvx_x = u * (1 + 0.5) + uvd_x * 0.5; 331 uvx_y = v * (1 + 0.5) + uvd_y * 0.5; 332 uvx_pos_x = uvx_x - pos_x; 333 uvx_pos_y = uvx_y - pos_y; 334 335 temp = uvx_pos_x * uvx_pos_x + uvx_pos_y * uvx_pos_y; 336 const f6 = Math.max(0.01 - temp * 0.9, 0.0) * 6.0; 337 const f62 = Math.max(0.01 - temp * 0.1, 0.0) * 3.0; 338 const f63 = Math.max(0.01 - temp * 0.12, 0.0) * 5.0; 339 340 imgdata.data[4 * i + 0] += 341 1.2 * ((f2 + f4 + f5 + f6) * 1.3 - uvd_len * 0.05) * intensity; 342 imgdata.data[4 * i + 1] += 343 1.5 * ((f22 + f42 + f52 + f62) * 1.3 - uvd_len * 0.05) * intensity; 344 imgdata.data[4 * i + 2] += 345 1.3 * ((f23 + f43 + f53 + f63) * 1.3 - uvd_len * 0.05) * intensity; 346 imgdata.data[4 * i + 3] = 255; 347 } 348 ctx.putImageData(imgdata, 0, 0); 349 } 350 351 /** 352 * Amply filters and lensflare to the optionally scaled image, to only 353 * calculate on pixels the user can see. 354 * 355 * @param {bool} viewport_scale - render image scaled to viewport or in full 356 * resolution 357 */ 358 function draw(viewport_scale) { 359 const filter = Object.entries(settings) 360 .map( 361 ([setting, { elements, filter }]) => 362 `${setting}(${filter(elements[0].value)})`, 363 ) 364 .join(" "); 365 366 // set the resolution to the original and then scale down to the viewport to 367 // only calculate the filter on pixels the user can see. 368 canvas.height = img.naturalHeight; 369 canvas.width = img.naturalWidth; 370 if (viewport_scale) { 371 canvas.height = canvas.clientHeight; 372 canvas.width = canvas.clientWidth; 373 } 374 ctx.filter = filter; 375 console.log(filter); 376 ctx.drawImage(img, 0, 0, canvas.width, canvas.height); 377 ctx.filter = ""; 378 379 if (lensflare_active) { 380 lensflare(cursor.x, cursor.y); 381 } 382 }