diff --git a/Assets/logo.png b/Assets/logo.png new file mode 100644 index 0000000..d821e46 Binary files /dev/null and b/Assets/logo.png differ diff --git a/README.md b/README.md index 0a1e2a9..652abaa 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # EditPix +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![NPM Test on Main](https://github.com/studio-YOLO/editpix/actions/workflows/main.yml/badge.svg)](https://github.com/studio-YOLO/editpix/actions/workflows/main.yml) A powerful and versatile image editor for [insert your target audience/users]. -## Features +![key1](Assets/logo.png) ### Basic Editing: diff --git a/demo/index.html b/demo/index.html index 980a254..5d21d8d 100644 --- a/demo/index.html +++ b/demo/index.html @@ -24,6 +24,7 @@ --> + \ No newline at end of file diff --git a/demo/scripts/color_palette_wasm_demo.js b/demo/scripts/color_palette_wasm_demo.js index 77c4de3..baa9d7a 100644 --- a/demo/scripts/color_palette_wasm_demo.js +++ b/demo/scripts/color_palette_wasm_demo.js @@ -12,16 +12,13 @@ container.classList.add("container") //waiting image load image.onload = () => { - //calculate color palette - editpix.getColorPaletteWasm(image, 13, 1) + console.log("Image loaded."); + + editpix.getColorPaletteWasm(image, 15, 2, "median cut") .then(colorPalette => { console.log(colorPalette) displayPalette(colorPalette); - }) - - - - + }); }; diff --git a/demo/scripts/higher_color_contrast_demo.js b/demo/scripts/higher_color_contrast_demo.js new file mode 100644 index 0000000..00fa679 --- /dev/null +++ b/demo/scripts/higher_color_contrast_demo.js @@ -0,0 +1,23 @@ +import EditPix from "../../src/editpix.js"; + +const editpix = new EditPix(); + +//color +const hexColor = "#78828c"; + +//convert color from hex to rgb +const rgbColor = editpix.convertToRgb(hexColor); + +//get the higher contrast color +const higherColorContrastRgb = editpix.getHigherContrast(rgbColor); + +//convert higher contrast color from rgb to hex +const higherColorContrastHex = editpix.convertToHex(higherColorContrastRgb); + + +//display results +document.body.style.backgroundColor = hexColor; +const contrastText = document.createElement("h1"); +contrastText.textContent = "Hello World!" +contrastText.style.color = higherColorContrastHex; +document.body.appendChild(contrastText) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index cf030f5..3b0d3a7 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -7,17 +7,11 @@ edition = "2021" [dependencies] wasm-bindgen = "0.2" +getrandom = { version = "0.2", features = ["js"] } +rand = "*" [lib] crate-type = ["cdylib", "rlib"] -[dependencies.web-sys] -version = "0.3.68" -features = [ - 'Document', - 'Element', - 'HtmlElement', - 'Node', - 'Window', -] + diff --git a/lib/src/.gitignore b/lib/src/.gitignore deleted file mode 100644 index 35c6713..0000000 --- a/lib/src/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -target/ -pkg/ -.DS_Store \ No newline at end of file diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 065c89c..2f38fe5 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,13 +1,13 @@ extern crate wasm_bindgen; use wasm_bindgen::prelude::*; -extern crate web_sys; +use rand::prelude::*; #[inline] fn euclidean_distance(color1: &[u8; 3], color2: &[u8; 3]) -> f64 { f64::sqrt( - ((color1[0] as i16 - color2[0] as i16) as f64).powi(2) + - ((color1[1] as i16 - color2[1] as i16) as f64).powi(2) + - ((color1[2] as i16 - color2[2] as i16) as f64).powi(2) + (color1[0] as f64 - color2[0] as f64).powi(2) + + (color1[1] as f64 - color2[1] as f64).powi(2) + + (color1[2] as f64 - color2[2] as f64).powi(2) ) } @@ -105,4 +105,191 @@ pub fn k_means(colors_r: Vec, color_number: usize, max_iterations: usize) -> .collect(); serialized_vector -} \ No newline at end of file +} + +fn square_distance(color1: &[u8; 3], color2: &[u8; 3]) -> f64 { + (color1[0] as f64 - color2[0] as f64).powi(2) + + (color1[1] as f64 - color2[1] as f64).powi(2) + + (color1[2] as f64 - color2[2] as f64).powi(2) +} + +fn initialize_centroids_pp(colors: &Vec<[u8; 3]>, color_number: usize) -> Vec<[u8; 3]> { + let mut rng = thread_rng(); + let mut centroids: Vec<[u8; 3]> = Vec::new(); + let first_centroid = colors[rng.gen_range(0..colors.len())]; + centroids.push(first_centroid); + + for _i in 1..color_number { + let distances = colors.iter().map(|x| { + let partial_distances = centroids.iter().map(|y| square_distance(x, y)); + partial_distances.fold(f64::INFINITY, |a, b| a.min(b)) + }); + let total_weight = distances.clone().fold(0.0, |a, b| a + b); + let distances: Vec<_> = distances.collect(); + let target = rng.gen::() * total_weight; + let mut cumulative = 0.0; + for i in 0..colors.len() { + cumulative += distances[i]; + if cumulative >= target { + centroids.push(colors[i]); + break; + } + } + } + + centroids +} + +fn assign_to_centroids_pp(colors: &[[u8; 3]], centroids: Vec<[u8; 3]>) -> Vec { + let mut assignments: Vec = Vec::new(); + for i in 0..colors.len() { + let mut min_distance = f64::INFINITY; + let mut closest_centroid = 0; + for j in 0..centroids.len() { + let distance = square_distance(colors.get(i).unwrap(), centroids.get(j).unwrap()); + if distance < min_distance { + min_distance = distance; + closest_centroid = j; + } + } + assignments.push(closest_centroid); + } + + assignments +} + +#[wasm_bindgen] +pub fn k_means_pp(colors_r: Vec, color_number: usize, max_iterations: usize) -> Vec { + let colors: Vec<[u8; 3]> = colors_r + .chunks_exact(3) // Get chunks of 3 elements + .map(|chunk| { + let mut array: [u8; 3] = [0; 3]; + array.copy_from_slice(chunk); + array + }) + .collect(); + + + let mut centroids = initialize_centroids_pp(&colors, color_number); + + let mut iterations: usize = 0; + let mut previous_assignments; + let mut assignments: Vec = Vec::new(); + + loop { + previous_assignments = assignments; + assignments = assign_to_centroids_pp(&colors, centroids); + centroids = calculate_new_centroids(&colors, &assignments, color_number); + iterations += 1; + if iterations > max_iterations || assignments == previous_assignments { + break; + } + } + + let serialized_vector: Vec = centroids + .into_iter() + .flat_map(|array| array.into_iter()) + .collect(); + + serialized_vector +} + + + + +// Sort an RGB pixel array by its channel with the highest variance +fn sort_pixels(pixels: &Vec<[u8; 3]>, channel: usize) -> Vec<[u8; 3]> { + let mut px = pixels.to_owned(); + px.sort_by(|a, b| a[channel].cmp(&b[channel])); + px +} + +// Find the color channel with the highest variance +fn find_max_channel(pixels: &Vec<[u8; 3]>) -> usize { + let mut min = [255, 255, 255]; + let mut max = [0, 0, 0]; + for (_i, pixel) in pixels.iter().enumerate() { + for j in 0..3 { + if pixel[j] < min[j] { + min[j] = pixel[j]; + } + if pixel[j] > max[j] { + max[j] = pixel[j]; + } + } + } + let mut range = [0, 0, 0]; + for j in 0..3 { + range[j] = max[j] - min[j]; + } + let max_channel: usize = (0..3).into_iter().max().unwrap(); + max_channel +} + +// Find the average color of an RGB pixel array +fn find_average_color(pixels: Vec<[u8; 3]>) -> [u8; 3] { + let mut sum: [f32; 3] = [0.0, 0.0, 0.0]; + for pixel in &pixels { + for j in 0..3 { + sum[j] += pixel[j] as f32; + } + } + let avg: [u8; 3] = [(sum[0] / pixels.len() as f32) as u8, (sum[1] / pixels.len() as f32) as u8, (sum[2] / pixels.len() as f32) as u8]; + avg +} + +// Apply the median cut algorithm to an RGB pixel array and return a downsized color palette +#[wasm_bindgen] +pub fn median_cut(pixels_r: Vec, palette_size: usize) -> Vec { + // Turn the linear array into an array of RGB arrays + let pixels: Vec<[u8; 3]> = pixels_r + .chunks_exact(3) // Get chunks of 3 elements + .map(|chunk| { + let mut array: [u8; 3] = [0; 3]; + array.copy_from_slice(chunk); + array + }) + .collect(); + + // Initialize a queue of regions with all pixels + let mut queue = vec![pixels]; + // Repeat the following loop until the queue reaches the correct size + while queue.len() < palette_size { + // Extract the region with the most pixels from the queue + let mut max_index = 0; + let mut max_size = 0; + for (i, region) in queue.iter().enumerate() { + if region.len() > max_size { + max_size = region.len(); + max_index = i; + } + } + let region = queue.remove(max_index); + // Find the channel with the highest variance within the region + let channel = find_max_channel(®ion); + // Sort the pixels in the region by that channel + let sorted: Vec<[u8; 3]> = sort_pixels(®ion, channel).iter().cloned().collect(); + // Find the average and bisect the region + let median = sorted.len() / 2; + let left = &sorted[..median]; + let right = &sorted[median..]; + // Add the two regions to the queue + queue.push(left.to_vec()); + queue.push(right.to_vec()); + } + // Compute the average color of each region and return the palette + let mut palette: Vec<[u8; 3]> = Vec::new(); + for region in queue { + let color = find_average_color(region); + palette.push(color); + } + + // Serialize the array of arrays to get a linear array + let serialized_vector: Vec = palette + .into_iter() + .flat_map(|array| array.into_iter()) + .collect(); + + serialized_vector +} + diff --git a/src/core/change_exposure.js b/src/core/change_exposure.js new file mode 100644 index 0000000..068ce60 --- /dev/null +++ b/src/core/change_exposure.js @@ -0,0 +1,23 @@ +/** + * Function to change the exposure of an image. + * @param {number[]} pixelArray: Image pixel array in the format [R, G, B, alpha,..., R, G, B, alpha]. + * @param {number} factor: Factor to adjust the exposure (-100 to 100). + * @returns {number[]} Pixel array of the image with adjusted exposure. + */ +function changeExposure(pixelArray, factor) { + for (let i = 0; i < pixelArray.length; i += 4) { + const r = pixelArray[i]; + const g = pixelArray[i + 1]; + const b = pixelArray[i + 2]; + + // Apply exposure adjustment to each channel + const newR = Math.max(0, Math.min(255, r + factor * 2.55)); // Factor scaled to range 0-255 + const newG = Math.max(0, Math.min(255, g + factor * 2.55)); + const newB = Math.max(0, Math.min(255, b + factor * 2.55)); + + pixelArray[i] = newR; + pixelArray[i + 1] = newG; + pixelArray[i + 2] = newB; + } + return pixelArray; +} diff --git a/src/core/change_tint.js b/src/core/change_tint.js new file mode 100644 index 0000000..8d22328 --- /dev/null +++ b/src/core/change_tint.js @@ -0,0 +1,23 @@ +/** + * Function to change the tint of an image. + * @param {number[]} pixelArray: Image pixel array in the format [R, G, B, alpha,..., R, G, B, alpha]. + * @param {number} tint: Tint to apply (-100 to 100). + * @returns {number[]} Pixel array of the image with adjusted tint. + */ +function changeTint(pixelArray, tint) { + for (let i = 0; i < pixelArray.length; i += 4) { + const r = pixelArray[i]; + const g = pixelArray[i + 1]; + const b = pixelArray[i + 2]; + + // Apply tint adjustment to each channel + const newR = Math.max(0, Math.min(255, r + (255 - r) * (tint / 100))); + const newG = Math.max(0, Math.min(255, g + (255 - g) * (tint / 100))); + const newB = Math.max(0, Math.min(255, b + (255 - b) * (tint / 100))); + + pixelArray[i] = newR; + pixelArray[i + 1] = newG; + pixelArray[i + 2] = newB; + } + return pixelArray; +} diff --git a/src/core/editpix_wasm.js b/src/core/editpix_wasm.js index 6085545..45dd590 100644 --- a/src/core/editpix_wasm.js +++ b/src/core/editpix_wasm.js @@ -1,5 +1,25 @@ let wasm; +const heap = new Array(128).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { return heap[idx]; } + +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 132) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; @@ -18,6 +38,15 @@ function getStringFromWasm0(ptr, len) { return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); } +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + let WASM_VECTOR_LEN = 0; function passArray8ToWasm0(arg, malloc) { @@ -62,6 +91,57 @@ export function k_means(colors_r, color_number, max_iterations) { } } +/** +* @param {Uint8Array} colors_r +* @param {number} color_number +* @param {number} max_iterations +* @returns {Uint8Array} +*/ +export function k_means_pp(colors_r, color_number, max_iterations) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArray8ToWasm0(colors_r, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + wasm.k_means_pp(retptr, ptr0, len0, color_number, max_iterations); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v2 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 1, 1); + return v2; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +/** +* @param {Uint8Array} pixels_r +* @param {number} palette_size +* @returns {Uint8Array} +*/ +export function median_cut(pixels_r, palette_size) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArray8ToWasm0(pixels_r, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + wasm.median_cut(retptr, ptr0, len0, palette_size); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v2 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 1, 1); + return v2; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} + async function __wbg_load(module, imports) { if (typeof Response === 'function' && module instanceof Response) { if (typeof WebAssembly.instantiateStreaming === 'function') { @@ -96,9 +176,122 @@ async function __wbg_load(module, imports) { function __wbg_get_imports() { const imports = {}; imports.wbg = {}; + imports.wbg.__wbg_crypto_d05b68a3572bb8ca = function(arg0) { + const ret = getObject(arg0).crypto; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_is_object = function(arg0) { + const val = getObject(arg0); + const ret = typeof(val) === 'object' && val !== null; + return ret; + }; + imports.wbg.__wbg_process_b02b3570280d0366 = function(arg0) { + const ret = getObject(arg0).process; + return addHeapObject(ret); + }; + imports.wbg.__wbg_versions_c1cb42213cedf0f5 = function(arg0) { + const ret = getObject(arg0).versions; + return addHeapObject(ret); + }; + imports.wbg.__wbg_node_43b1089f407e4ec2 = function(arg0) { + const ret = getObject(arg0).node; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_is_string = function(arg0) { + const ret = typeof(getObject(arg0)) === 'string'; + return ret; + }; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; + imports.wbg.__wbg_msCrypto_10fc94afee92bd76 = function(arg0) { + const ret = getObject(arg0).msCrypto; + return addHeapObject(ret); + }; + imports.wbg.__wbg_require_9a7e0f667ead4995 = function() { return handleError(function () { + const ret = module.require; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbindgen_is_function = function(arg0) { + const ret = typeof(getObject(arg0)) === 'function'; + return ret; + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; + imports.wbg.__wbg_randomFillSync_b70ccbdf4926a99d = function() { return handleError(function (arg0, arg1) { + getObject(arg0).randomFillSync(takeObject(arg1)); + }, arguments) }; + imports.wbg.__wbg_getRandomValues_7e42b4fb8779dc6d = function() { return handleError(function (arg0, arg1) { + getObject(arg0).getRandomValues(getObject(arg1)); + }, arguments) }; + imports.wbg.__wbg_newnoargs_e258087cd0daa0ea = function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_call_27c0f87801dedf93 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).call(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbindgen_object_clone_ref = function(arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); + }; + imports.wbg.__wbg_self_ce0dbfc45cf2f5be = function() { return handleError(function () { + const ret = self.self; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_window_c6fb939a7f436783 = function() { return handleError(function () { + const ret = window.window; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_globalThis_d1e6af4856ba331b = function() { return handleError(function () { + const ret = globalThis.globalThis; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_global_207b558942527489 = function() { return handleError(function () { + const ret = global.global; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbindgen_is_undefined = function(arg0) { + const ret = getObject(arg0) === undefined; + return ret; + }; + imports.wbg.__wbg_call_b3ca7c6051f9bec1 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_buffer_12d079cc21e14bdb = function(arg0) { + const ret = getObject(arg0).buffer; + return addHeapObject(ret); + }; + imports.wbg.__wbg_newwithbyteoffsetandlength_aa4a17c33a06e5cb = function(arg0, arg1, arg2) { + const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); + return addHeapObject(ret); + }; + imports.wbg.__wbg_new_63b92bc8671ed464 = function(arg0) { + const ret = new Uint8Array(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_set_a47bac70306a19a7 = function(arg0, arg1, arg2) { + getObject(arg0).set(getObject(arg1), arg2 >>> 0); + }; + imports.wbg.__wbg_newwithlength_e9b4878cebadb3d3 = function(arg0) { + const ret = new Uint8Array(arg0 >>> 0); + return addHeapObject(ret); + }; + imports.wbg.__wbg_subarray_a1f73cd4b5b42fe1 = function(arg0, arg1, arg2) { + const ret = getObject(arg0).subarray(arg1 >>> 0, arg2 >>> 0); + return addHeapObject(ret); + }; imports.wbg.__wbindgen_throw = function(arg0, arg1) { throw new Error(getStringFromWasm0(arg0, arg1)); }; + imports.wbg.__wbindgen_memory = function() { + const ret = wasm.memory; + return addHeapObject(ret); + }; return imports; } diff --git a/src/core/editpix_wasm_bg.wasm b/src/core/editpix_wasm_bg.wasm index b34cbfd..aa02243 100644 Binary files a/src/core/editpix_wasm_bg.wasm and b/src/core/editpix_wasm_bg.wasm differ diff --git a/src/core/higher_contrast.js b/src/core/higher_contrast.js index 2d14923..befed5e 100644 --- a/src/core/higher_contrast.js +++ b/src/core/higher_contrast.js @@ -1,21 +1,13 @@ /** * Function that given a color in the format [R,G,B] converts it to grayscale in the format [R,G,B] - * and then calculates the color with the highest contrast. + * and then calculates the color (black or white) with the highest contrast. * @param {number[]} color Color that has to be converted and compared in the format [R,G,B] * @returns {number[]} Color with the higher contrast of the input color in the format [R,G,B] */ function higherColorContrast(color) { - // Convert the color to grayscale - const grayscale = [ - Math.round(0.299 * color[0] + 0.587 * color[1] + 0.114 * color[2]), - Math.round(0.299 * color[0] + 0.587 * color[1] + 0.114 * color[2]), - Math.round(0.299 * color[0] + 0.587 * color[1] + 0.114 * color[2]) - ]; - - // Calculate the color with the highest contrast - const contrastColor = grayscale.map(channel => channel > 128 ? 0 : 255); - - return contrastColor; + const gray = Math.round(0.299 * color[0] + 0.587 * color[1] + 0.114 * color[2]) + const contrastColor = gray >= 128 ? 0 : 255; + return [contrastColor, contrastColor, contrastColor]; } export default higherColorContrast; diff --git a/src/editpix.js b/src/editpix.js index 82e2b25..1a9726f 100644 --- a/src/editpix.js +++ b/src/editpix.js @@ -4,7 +4,7 @@ import convertToBW from "./core/black_and_white.js"; import kMeans from "./core/kmean.js"; import imageManager from "./image_manager.js"; import higherColorContrast from "./core/higher_contrast.js"; -import init, { k_means } from "./core/editpix_wasm.js" +import init, { k_means, k_means_pp, median_cut } from "./core/editpix_wasm.js" import optimizeContrast from "./core/optimize_contrast.js"; import changeContrast from "./core/change_contrast.js"; import changeTemperature from "./core/change_temperature.js"; @@ -27,12 +27,29 @@ EditPix.prototype.getColorPalette = (image, colorNumber = 5, quality = 1) => { }) } -EditPix.prototype.getColorPaletteWasm = async (image, colorNumber = 5) => { - utils.validate(1, colorNumber); - const pixelArray = utils.removeAlphaSerialized(imageManager.getPixelArray(image)); - await init(); - let a = k_means(pixelArray, colorNumber, 100); - return utils.deserializeArray(a); +EditPix.prototype.getColorPaletteWasm = async (image, colorNumber = 5, quality = 1, algorithm = "k-means++") => { + return new Promise((resolve, reject) => { + utils.validate(quality, colorNumber); + imageManager.resizeByPercentage(image, quality * 10) + .then(resizedImage => { + const pixelArray = utils.removeAlphaSerialized(imageManager.getPixelArray(resizedImage)); + if (algorithm === "k-means") { + init().then(() => { + resolve(utils.deserializeArray(k_means(pixelArray, colorNumber, 100))); + }) + } else if (algorithm === "k-means++") { + init().then(() => { + resolve(utils.deserializeArray(k_means_pp(pixelArray, colorNumber, 100))); + }) + } else if (algorithm === "median cut") { + init().then(() => { + resolve(utils.deserializeArray(median_cut(pixelArray, colorNumber))); + }) + } else { + throw new Error("Non-existent algorithm."); + } + }) + }).catch(error => { reject(error) }) } EditPix.prototype.getDominantColor = function (image, quality = 1) { diff --git a/test/core.test.js b/test/core.test.js index 45ae520..07fb1ea 100644 --- a/test/core.test.js +++ b/test/core.test.js @@ -5,6 +5,7 @@ import changeContrast from "../src/core/change_contrast.js"; import changeTemperature from "../src/core/change_temperature.js"; import changeOpacity from "../src/core/change_opacity.js"; import changeShadows from "../src/core/change_shadows.js" +import higherColorContrast from "../src/core/higher_contrast.js"; describe('convertToBW function', () => { test('converts pixel array to black and white correctly', () => { @@ -134,6 +135,7 @@ describe('changeOpacity function', () => { }); }); + describe('changeShadows', () => { test('should return a darkened array if factor is negative and area is a shadow', () => { const testColor1 = [13, 11, 4]; @@ -158,5 +160,32 @@ describe('changeShadows', () => { expect(testColor1[0]).toEqual(testColor2[0]); expect(testColor1[1]).toEqual(testColor2[1]); expect(testColor1[2]).toEqual(testColor2[2]); + +describe('higherColorContrast', () => { + test('should return color with higher contrast for dark input color', () => { + const darkColor = [10, 20, 30]; + const expectedResult = [255, 255, 255]; // Expected result for dark color + + const result = higherColorContrast(darkColor); + + expect(result).toEqual(expectedResult); + }); + + test('should return color with higher contrast for light input color', () => { + const lightColor = [200, 210, 220]; + const expectedResult = [0, 0, 0]; // Expected result for light color + + const result = higherColorContrast(lightColor); + + expect(result).toEqual(expectedResult); + }); + + test('should return color with higher contrast for medium input color', () => { + const mediumColor = [120, 130, 140]; + const expectedResult = [0, 0, 0]; // Expected result for medium color + + const result = higherColorContrast(mediumColor); + + expect(result).toEqual(expectedResult); }); }); \ No newline at end of file