imagine

a simple image editor
git clone git://source.orangerot.dev:/university/imagine.git
Log | Files | Refs | README | LICENSE

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 }