Pour tout problème contactez-nous par mail : support@froggit.fr | La FAQ :grey_question: | Rejoignez-nous sur le Chat :speech_balloon:

Skip to content
Snippets Groups Projects
Commit da194280 authored by Pierre Jarriges's avatar Pierre Jarriges
Browse files

docstrings + clean up

parent 11ef060e
No related branches found
No related tags found
No related merge requests found
"use strict";
/**
* Helpers to works with color values
*/
/**
* Get a RGB color value in String from either "rgb(x,x,x)" or #xxxxxx
* @param {String} color A RGB color value as String in the form: #ffffff or rgb(x,x,x)
* @returns {Array<Uint8>} An array of 3 values from 0 to 256 for [RED, GREEN, BLUE]
*/
function color_str_to_rgb_array(color) {
color = color.toLowerCase(0);
if (color.includes("rgb")) {
......@@ -15,10 +23,19 @@ function color_str_to_rgb_array(color) {
}
}
/**
* @param {String} color A color value (#xxxxxx or rbg(x,x,x))
* @returns The average between each tone of the given color.
*/
function get_average_rgb_color_tone(color) {
return color_str_to_rgb_array(color).reduce((tot, n) => tot + n / 3, 0);
}
/**
* Gets an array of 3 8bits integers for red green and blue and and return a color value as an hexadecimal string #xxxxxx
* @param {Array<Uint8>} rgb
* @returns {String} The hexacdecimal rgb value of the color including the hash character: #xxxxxx
*/
function rgb_array_to_hex(rgb) {
return `#${rgb.slice(0, 3).map(n => {
const hex = n.toString(16);
......@@ -26,6 +43,11 @@ function rgb_array_to_hex(rgb) {
}).join('')}`;
}
/**
* Gets an array of 4 8bits integers for red green blue and transparency channel and return a color value as an hexadecimal string #xxxxxx
* @param {Array<Uint8>} rgb
* @returns {String} The hexacdecimal rgba value of the color including the hash character: #xxxxxxxx
*/
function rgba_array_to_hex(rgba) {
rgba = rgba.length === 4 ? rgba : rgba.concat([255])
return `#${rgba.slice(0, 4).map(n => {
......@@ -34,6 +56,12 @@ function rgba_array_to_hex(rgba) {
}).join('')}`;
}
/**
* Gets a background color as argument and return a calculated optimal foreground color.
* For example if the background is dark the function will return a lighten version of the background.
* @param {String} base_color A RGB color value in hexadecimal form: #ffffff
* @returns
*/
function get_optimal_visible_foreground_color(base_color) {
const tmp_canvas = document.createElement("canvas");
tmp_canvas.width = 1;
......@@ -47,6 +75,11 @@ function get_optimal_visible_foreground_color(base_color) {
return rgb_array_to_hex(Array.from(ctx.getImageData(0, 0, 1, 1).data));
}
/**
* @param {Array<Uint8; 4>} col1 A RGBA color value as an array of 4 0 to 256 integers
* @param {Array<Uint8; 4>} col2 A RGBA color value as an array of 4 0 to 256 integers
* @returns {Boolean} true if the 2 colors are equals
*/
function same_rgba(col1, col2) {
return col1[0] === col2[0]
&& col1[1] === col2[1]
......
{
"name": "mentalo-drawing-tool",
"version": "0.2.3",
"version": "0.2.4",
"description": "A minimalistic pixel art drawing and animation tool ",
"main": "index.js",
"scripts": {
......
"use strict";
class FrameRateController {
constructor(fps) {
this.tframe = performance.now();
this.interval = 1000 / fps;
this.initial = true;
}
nextFrameReady() {
if (this.initial) {
this.initial = false;
return true;
}
const now = performance.now();
const elapsed = now - this.tframe;
const ready = elapsed > this.interval;
if (ready) {
this.tframe = now - (elapsed % this.interval);
}
return ready;
}
}
const FrameRateController = require("./frame-rate-controller");
/**
* Controls an animation displaying state
*/
class AnimationState {
/**
* @param {Object} params
* Expected params
* refresh_tools: {Function}
refresh_tabs: {Function}
refresh_animation: {Function}
get_frames: {Function}
*/
constructor(params) {
this.params = params;
this.on = false;
......@@ -40,11 +31,18 @@ class AnimationState {
};
}
/**
* Updates the frame count
*/
update_player_framecount() {
const { framecount } = this.player;
this.player.framecount = framecount + 1 <= Number.MAX_SAFE_INTEGER ? framecount + 1 : 0;
}
/**
* Starts playing the animation sequence.
* Calls requestAnimationFrame.
*/
play_animation() {
let init = false;
const { get_frames, refresh_tools, refresh_tabs, refresh_animation } = this.params;
......@@ -78,7 +76,9 @@ class AnimationState {
window.mentalo_drawing_tool_animation_id = requestAnimationFrame(this.play_animation.bind(this))
}
/**
* Stops playing the animation sequence
*/
stop_animation() {
const { refresh_tools, refresh_animation } = this.params;
cancelAnimationFrame(window.mentalo_drawing_tool_animation_id);
......@@ -87,6 +87,11 @@ class AnimationState {
refresh_animation();
}
/**
* Handle changes of the onion skin mode.
* Will show previous frame, next, or both on an additional transparent layer.
* @param {String} option
*/
handle_show_onion_skin(option) {
const { refresh_tools, refresh_animation } = this.params;
......
......@@ -2,6 +2,9 @@
const { editable_file_mime } = require("../constants");
/**
* The data type used to export a drawing project
*/
class DrawingProjectExport {
constructor(drawing_project) {
const ref = drawing_project;
......@@ -16,14 +19,25 @@ class DrawingProjectExport {
};
}
/**
* Serializes the instance to json
* @returns {JSON}
*/
serialized() {
return JSON.stringify(this)
}
/**
* Get the instance data as a json file.
* @returns {Blob}
*/
to_blob() {
return new Blob([this.serialized()], { type: "application/json" })
}
/**
* PAck the instance into a blob and triggers the downloading of the data.
*/
export_download() {
const link = document.createElement("a");
const url = URL.createObjectURL(this.to_blob());
......
"use strict";
/**
* A timer
*/
class FrameRateController {
constructor(fps) {
this.tframe = performance.now();
this.interval = 1000 / fps;
this.initial = true;
}
/**
* @returns {Boolean} true if the time elapsed since last call is greater or equal than the interval set in constructor
*/
nextFrameReady() {
if (this.initial) {
this.initial = false;
return true;
}
const now = performance.now();
const elapsed = now - this.tframe;
const ready = elapsed > this.interval;
if (ready) {
this.tframe = now - (elapsed % this.interval);
}
return ready;
}
}
module.exports = FrameRateController;
\ No newline at end of file
......@@ -6,6 +6,10 @@ const Layer = require("./layer");
const translator = require("../lib/translator");
const t = translator.trad.bind(translator);
/**
* Performs operations on a frame (ImageData) list,
* manages history, clipboard and drawing operations.
*/
class FramesManager {
constructor(params) {
this.params = params;
......@@ -26,34 +30,61 @@ class FramesManager {
};
}
/**
* Initialize the instance with a single blank frame
*/
init() {
this.items = [this.get_new_image_data()];
}
/**
* Removes every frame item
*/
empty() {
this.items = [];
}
/**
* Adds a new frame at the end of the items list.
* @param {ImageData} frame_data
*/
add(frame_data) {
this.items.push(frame_data);
}
/**
* Set all frames at once
* @param {Array<ImageData>} items
*/
set_items(items) {
this.items = items;
}
/**
* Saves the current state in history and erases a frame data.
* @param {Integer} index The index of the frame to clear.
*/
clear_frame(index) {
this.save_history();
this.get_frame(index).data.fill(this.init_fill_color[0])
}
/**
* Removes a frame from items.
* @param {Integer} index The index of the frame to delete
*/
delete_frame(index) {
this.save_history();
this.items.splice(index, 1);
}
/**
* Inserts a new frame to a given index
* @param {Integer} index_push The insertion index
* @param {Boolean} copy Wether the new frame must be a copy of the current frame or a blank frame.
*/
insert_frame(index_push, copy = false) {
this.save_history();
......@@ -64,12 +95,18 @@ class FramesManager {
: this.items.splice(index_push, 0, new_frame);
}
/**
* Copies the current frame into clipboard state.
*/
copy_frame() {
const copy = this.get_new_image_data(true);
this.state.clipboard.frame = copy;
this.state.clipboard.is_empty = false;
}
/**
* Copy the data from clipboard to the current frame data.
*/
paste_frame() {
if (!this.state.clipboard.is_empty) {
this.save_history();
......@@ -92,10 +129,20 @@ class FramesManager {
}
}
/**
* Get a frame data at a given index
* @param {Integer} index
* @returns {ImageData}
*/
get_frame(index) {
return this.items[index];
}
/**
* Get a new frame as ImageData
* @param {Boolean} copy Wether the new frame must be a copy of the current frame or not
* @returns {ImageData}
*/
get_new_image_data(copy = false) {
const canvas_metrics = this.params.get_canvas_metrics();
const edit_index = this.params.get_edit_frame_index();
......@@ -119,11 +166,20 @@ class FramesManager {
return img_data;
}
/**
* Get the data of the currently edited frame
* @returns {ImageData}
*/
get_edit_image_data() {
const edit_index = this.params.get_edit_frame_index();
return this.items[edit_index];
}
/**
* Get a pixel in the currently edited frame and replaces its color.
* @param {Integer} i The index of the pixel to update
* @param {Array<Uint8;4>} color The color to put in the pixel
*/
draw_at_index(i, color) {
const image_data = this.get_edit_image_data();
image_data.data[i] = color[0];
......@@ -132,6 +188,11 @@ class FramesManager {
image_data.data[i + 3] = color[3] !== undefined ? color[3] : 255;
}
/**
* Get the pixel at the given index in the currently edited frame and returns the color value.
* @param {Integer} i The index of the wanted pixel
* @returns {Array<Uint8;4>} The color of the pixel
*/
pick_color_at_index(i) {
const image_data = this.get_edit_image_data();
return [
......@@ -142,11 +203,23 @@ class FramesManager {
];
}
/**
* Calculates the index of a pixel in a matrix from an x y position.
* @param {Integer} x
* @param {Integer} y
* @returns {Integer}
*/
get_index_from_coords(x, y) {
const canvas_metrics = this.params.get_canvas_metrics();
return (x + (canvas_metrics.draw_image_data.width * y)) * 4;
}
/**
* Calculates the bounding box of the pointer from its size and position
* and updates the corresponding pixel with the given color.
* @param {Object<x:Integer, y:Integer>} pointer The x y position of the pointer
* @param {Array<Uint8;4>} use_color The color data to use
*/
draw_on_pointer_zone(pointer, use_color) {
const pen_size = this.params.get_pen_size();
......@@ -186,10 +259,19 @@ class FramesManager {
}
}
/**
* Clears the line.prev_point state
*/
reset_line() {
this.state.line.prev_point = undefined;
}
/**
* Updates the pixels on a straight line between the coordinates of the
* previous captured mouse event and the current one.
* @param {Object<x:Integer,y:Integer>} pointer The x y position of the mouse
* @param {Array<Uint8;4>} use_color The color data to use
*/
line_to(pointer, use_color) {
if (!this.state.line.prev_point) {
this.draw_on_pointer_zone(pointer, use_color);
......@@ -214,6 +296,12 @@ class FramesManager {
this.state.line.prev_point = { ...pointer };
}
/**
* Updates all contiguous pixels of same color aroud the given starting coordinates.
* @param {Object<x:Integer,y:Integer} coords The starting point of the algorithm
* @param {Array<Uint8;4>} target_color The color to convert
* @param {Array<Uint8;4>} fill_color The replacement color
*/
bucket_fill_from_coords(coords, target_color, fill_color) {
const pixel_index = this.get_index_from_coords(coords.x, coords.y);
const pixel_color = this.pick_color_at_index(pixel_index);
......@@ -246,6 +334,11 @@ class FramesManager {
}
}
/**
* Handles mouse events on canvas.
* @param {Event} e
* @param {Boolean} recording Wether this mouse event should be taken in account in the drawing
*/
draw(e, recording) {
const pen_size = this.params.get_pen_size(),
canvas_metrics = this.params.get_canvas_metrics();
......@@ -319,6 +412,9 @@ class FramesManager {
}
}
/**
* Makes a copy of the current frames in the history
*/
save_history() {
const save_frames = [];
......@@ -335,6 +431,10 @@ class FramesManager {
}
}
/**
* Replaces the current frames by the frames from the last history item.
* @param {Function} on_save
*/
restore_last_history(on_save) {
if (this.history.length > 0) {
const restore_frames = this.history.pop();
......
"use strict";
/**
* An abstraction of a canvas with given dimensions and its 2D context
*/
class Layer {
/**
* Initializes an HTML canvas element and references its 2D context.
* @param {Integer} width
* @param {Integer} height
* @param {Object} attrs
*/
constructor(width, height, attrs = {}) {
this.canvas = document.createElement("canvas");
......@@ -22,6 +31,10 @@ class Layer {
this.ctx = this.get_drawing_context();
}
/**
* Returns the 2D drawing context of the created canvas with settings to optimize pixel-art rendering
* @returns {CanvasRenderingContext2D}
*/
get_drawing_context() {
const ctx = this.canvas.getContext("2d");
ctx.mozImageSmoothingEnabled = false;
......@@ -31,16 +44,27 @@ class Layer {
return ctx;
}
/**
* Return the image data of the canvas as a base64 string.
* @returns {String}
*/
as_data_url() {
return this.canvas.toDataURL();
}
/**
* Returns the canvas image data as an HTML image element
* @returns {Image}
*/
as_image() {
const img = new Image();
img.src = this.as_data_url();
return img;
}
/**
* Clears the contents of the canvas draing context.
*/
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
......
......@@ -12,8 +12,13 @@ const translator = require("../lib/translator");
const t = translator.trad.bind(translator)
const UiComponent = require("./ui-component");
/**
* An interface component that renders the tools to handle animation mode
*/
class AnimationToolsCpt extends UiComponent {
/**
* @returns {Object} the object representation of the html to render for the clickable animation frame tabs
*/
render_animation_frame_tabs() {
const { playing, player, onion_skin } = this.params.get_animation_state();
const { play_frame } = player;
......@@ -38,6 +43,9 @@ class AnimationToolsCpt extends UiComponent {
};
}
/**
* Refreshes the displaying of the animation frame tabs
*/
refresh_animation_frame_tabs() {
this.params.renderer.subRender(
this.render_animation_frame_tabs(),
......@@ -46,6 +54,9 @@ class AnimationToolsCpt extends UiComponent {
);
}
/**
* @returns {Object} the object representation of the html to render for the animation speed current value
*/
render_fps_tip() {
const { player, frequency } = this.params.get_animation_state();
const { fps } = player
......@@ -60,6 +71,9 @@ class AnimationToolsCpt extends UiComponent {
}
}
/**
* @returns {Object} the object representation of the html to render for this component
*/
render() {
const { on, frequency, playing, onion_skin } = this.params.get_animation_state();
......
......@@ -2,6 +2,9 @@
const UiComponent = require("./ui-component");
/**
* The interface component that displays the container for the canvas.
*/
class CanvasContainerCpt extends UiComponent {
render() {
const { get_canvas_metrics } = this.params;
......
......@@ -14,7 +14,13 @@ const iconPaste = require("../jsvg/icon-paste");
const iconCancelArrow = require("../jsvg/icon-cancel-arrow");
const t = translator.trad.bind(translator);
/**
* The interface component that displays the sidebar containing the different drawing tools
*/
class DrawingToolBoxCpt extends UiComponent {
/**
* @returns {Object} the object representation of the html to render for this component
*/
render() {
const {
get_color,
......
......@@ -9,10 +9,35 @@ const translator = require("../lib/translator");
const t = translator.trad.bind(translator);
const UiComponent = require("./ui-component");
/**
* The interface component that displays the buttons to export and import files.
*/
class FileToolsCpt extends UiComponent {
/**
* Handles changes from fil input to import an image project
* @param {Event} e
*/
handle_import_file(e) {
const { on_import } = this.params;
if (!e.target.files) {
alert(t("No file selected"));
return;
} else if (typeof FileReader !== "function") {
alert(t("The File API is not supported by your browser."));
return;
}
const file_reader = new FileReader();
file_reader.onload = () => on_import(file_reader.result);
file_reader.readAsText(e.target.files[0]);
}
/**
* @returns {Object} the object representation of the html to render for this component
*/
render() {
const { on_export, on_import, on_go_back_with_output, on_go_back_discard } = this.params;
const { on_export, on_go_back_with_output, on_go_back_discard } = this.params;
return {
tag: "div",
id: "mtlo-dt-topbar",
......@@ -26,19 +51,7 @@ class FileToolsCpt extends UiComponent {
style_rules: {
display: "none"
},
onchange: e => {
if (!e.target.files) {
alert(t("No file selected"));
return;
} else if (typeof FileReader !== "function") {
alert(t("The File API is not supported by your browser."));
return;
}
const file_reader = new FileReader();
file_reader.onload = () => on_import(file_reader.result);
file_reader.readAsText(e.target.files[0]);
}
onchange: this.handle_import_file.bind(this)
},
].concat([
{
......
"use strict";
/**
* This class is meant to be extended by real components and
* This class is meant to be extended by the actual interface components and
* provide them a refresh method, an id field and a param field.
*
* /!\ The classes that extend this one must define a render() method
* that returns an object representation of the html to render.
*/
class UiComponent {
constructor(params) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment