jQUERY CANVAS

Lucas Villani Justo - 2024

O jQuery Canvas utiliza a tecnologia canvas do HTML5 aliada ao jQuery para realizar desenhos simples. O programa é orientado a objetos, na qual cada ferramenta é um objeto.

  1. Príncipio de funcionamento procedural
    1. Inicialização
      1. Canvas
      2. Seção de ferramentas no documento
      3. Carregando o Canvas
      4. Dicionário de ponteiros para as ferramentas e ferramenta atual
      5. Customização das características da ferramentas e dados relevantes
    2. Eventos no canvas
      1. Evento do desenho (mousedown mousemove)
      2. Final do desenho (mouseup mouseleave)
  2. Classes de Ferramentas
  3. Ações (Desfazer, Refazer e Limpar Desenho)

Princípio de funcionamento procedural

Inicialização

Canvas
                    
                        const canvas = document.querySelector("canvas");
                        ctx = canvas.getContext("2d");
                        ctx.imageSmoothingEnabled = true;
                    
                

A inicialização do canvas e do contexto do desenho são as primeiras coisas realizadas no código JS. ctx é o contexto a qual passaremos toda configuração e mudança que será aplicada ao canvas. Uma delas é habilitar a imageSmoothing que irá reduzir serrilhados no desenho.

Ferramentas no documento HTML
                    
                        const ferramentas = document.getElementById("ferramentas");
                        const brushSize = document.getElementById("brushSize");
                        const acoes = document.getElementById("acoes");
                    
                

Após isso definimos as regiões na qual haverá o seletor de ferramentas para o usuário dentro do arquivo.

A primeira constante, ferramentas é uma div que possui um série de botões que permite o usuário trocar a ferramenta atual.

A segunda se refere a um input do tipo range que é usado para manipular o tamanho do pincel, ou seja, a espessura das ferramentas .

A terceira a ações pertinentes a desfazer, refazer e limpar desenho.

Load e definição das dimensões do canvas
                    
                        window.addEventListener("load", () => {
                            canvas.width = canvas.offsetWidth;
                            canvas.height = canvas.offsetHeight;
                            
                            ctx.fillStyle = "#fff"; // Define a cor de fundo como branco
                            ctx.fillRect(0, 0, canvas.width, canvas.height);
                            
                            //Inicia o deslizante Brushsize e seta os valores de ínicio
                            brushSize.value = 5;
                            brushSize.min = 1;
                            brushSize.max = 100;
                            
                            //Seta valor de ínicio no seletor de cores
                            document.getElementById("brushColor").value = "#000";
                        });
                    
                

Chamada imediatamente, carrega o tamanho do canvas para que as coordenadas estarem em relação a ele mesmo e seu próprio tamanho. Incluindo a cor de fundo dele quenesse caso é branca, o tamanho atual, mínimo, máximo e a cor padrão do pincel.

Dicionário de ferramentas e ferramenta atual
                    
                        const tools = {
                            "lapis": new Pencil(),
                            "borracha": new Eraser(),
                            "retangulo": new Rectangle(),
                            "retangulo_fill": new RectangleFill(),
                            "elipse": new Ellipse(),
                            "elipse_fill": new EllipseFill(),
                            "linha": new Line(),
                            "balde": new Bucket()
                        };
                        
                        var current_tool = "lapis";
                        
                        const simpleDo = new SimpleDo(10, ctx.getImageData(0, 0, canvas.width, canvas.height));
                    
                

Aqui inicializamos todas as ferramentas que serão utilizadas pelo canvas, em seguida a variável current_tool que será usada como a chave para sabermos qual ferramenta o usuário deseja usar.
Também iniciamos o simpleDo, que é o objeto responsável por gerenciar as ações de desfazer e refazer
Um pouco mais a frente na explicação de como as classes de ferramentas funcionam, será mostrado que todas elas possuem métodos em comum que são chamados sequecialmente sem exceção. A combinação dessas duas entidades (tool e current_tool) permite que o código fique menor. Pois basta sabermos para qual ferramenta estamos apontando, pois os métodos serão os mesmos. Por exemplo:

                    
                        tools[current_tool].aplicar(ctx, e);
                        
                        //A explicação do "ctx" e do "e" será fornecida mais a frente.
                    
                

Para o lápis o comportamento no canvas será desenhar, para a borracha será apagar, para a linha será desenhar uma linha.

Portanto podemos trabalhar somente nas classes e deixar o código procedural do documento intacto.

Customização e dados importantes para o canvas
                    
                        const custom = {
                            //Dados gerais
                            "brush_size": 1,
                            "brush_color": "#000",
                            
                            //Dados para ferramentas: Retângulo, Linha, ELipse
                            "prevX": 0,
                            "prevY": 0,
                            
                            "snapshot": ctx.getImageData(0, 0, canvas.width, canvas.height),
                            
                            "canvas_size": {
                
                                "width": ctx.canvas.width,
                                "height": ctx.canvas.height
                                
                            },
                            
                            "limpo": true
                        };
                    
                

O usuário tem a possibilidade de alterar o tamanho do pincel e sua cor. Esses dados são armazenados no dicionário custom. O dicionário então será utilizado pelas ferramentas no método setup que define para cada ferramenta sua cor, tamanho e outros dados do dicionaŕio se necessário. Também funciona como armazenamento do valor da cor atual e do tamanho do pincel atual.

Já as chaves prevX, prevY e snapshot serão utilizadas para as ferramentas que deslizam, como retângulo, elipse, linha. O prevX e o prevY armazenam a posição inicial, ou seja, o início de onde será desenhado aquela forma. A snapshot serve para armazenar a imagem do desenho no instante antes das ferramentas a pouco citadas. Ela é chamada continuamente para gerar a ilusão que a forma desenhada está deslizando seguindo o mouse pelo canvas, até que o botão seja solto e a forma enfim desenhada de fato. Canvas width e canvas size é utilizado para passar a informação do tamanho do canvas para as ferramentas. Já o "limpo" se refere a ação limpar desenho, não faz sentido limpar o desenho mais de uma vez se ele já está limpo, então isso é uma flag que indica que o desenho já está limpo.

                    
                        //Para trocar a ferramenta
                        $(ferramentas).on("click", function (e) { current_tool = e.target.id; });

                        //Para trocar o tamanho da ferramenta
                        $(brushSize).on("input", function () { custom["brush_size"] = $(this).val(); });
                        
                        //Para trocar a cor da ferramenta
                        $(brushColor).on("input", function () { custom["brush_color"] = $(this).val(); });
                        
                        //Para lidar com o desfazer, refazer e limpar desenho
                        $(acoes).on("click", function(e) {
            
                            acao = e.target.id;
                            
                            switch (acao) {
                                
                                case "undo": 
                                    simpleDo.undo(ctx);
                                    break;
                                    
                                case "redo": 
                                    simpleDo.redo(ctx);
                                    break;
                                    
                                case "limpar":
                                
                                    //Limpar a tela é uma ação, mas só pode ser realizada uma vez (não faz sentido limpar um desenho que já foi limpo)
                                    if (custom["limpo"]) break;
                                    
                                    //Do contrário realizamos a limpeza
                                    ctx.fillStyle = "#fff"; // Define a cor de fundo como branco
                                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                                    
                                    simpleDo.action = true;
                                    custom["limpo"] = true;
                                    
                                    simpleDo.pushAction(ctx.getImageData(0, 0, canvas.width, canvas.height));
                                    break;
                                    
                                default:
                                    break;
                                
                            }
                        
                        });
                    
                

Após a definição de todos essas constantes e variáveis adicionamos os eventos que irão escutar as interações do usuário com as seções citadas no ínicio (ferramentas, brushSize, brushColor). Quando o usuário interagir com esses elementos o novo valor definido pelo usuário será salvo no dicionário custom para ser utilizado pelos objetos (ferramentas de desenho).
No caso das ações desfazer e refazer, simplesmente chamamos os métodos do objeto simpleDo.
Já no limpar, verificams primeiramente se o desenho já está limpo, se já estiver, não precisamos limpar novamente. Do contrário limpamos o desenho e consideramos aquilo uma nova ação, setamos a flag de limpo como limpo, e damos um push daquela ação no stack de ações do objeto simpleDo.

Eventos no canvas

Desenho (mousedown)

Primeiramente é redefinido o conteúdo das chaves snapshot, prevX, e prevYm pois elas só são relevantes serem modificadas na iminência da aplicação da ferramenta. Justamente porque marcam o ponto inicial onde o usuário começou a desenhar, como tira um retrato do desenho antes da aplicação da ferramenta, para que seja aplicado o desfazer e refazer, ou para criar a ilusaõ de deslize no caso das ferramentas, retângulo, elipse e linha. Em seguida chamamos o método beginPath() do ctx para dizermos que a aplicação da ferramenta deve iniciar onde o usuário deu o clique, e não a partir de onde ele terminou a aplciação da ferramenta anterior.

                    
                        $(canvas).on('mousedown', function(e) {

            
                            custom["snapshot"] = ctx.getImageData(0, 0, canvas.width, canvas.height);
                            custom["prevX"] = e.offsetX; 
                            custom["prevY"] = e.offsetY;
                            
                            simpleDo.action = true;
                            custom["limpo"] = false;
                            
                            ctx.beginPath(); 
                    
                

Quando o usuário realizar um clique com o botão esquerdo no canvas a ferramenta atual selecionada deve ser inicializada, ou seja, os parãmetros necessaŕios do dicionário custom serão aproveitados por aquela ferramenta, como o tamanho do lápis, a cor, e etc...
O simpleDo.action é setado para true para indicar que o usuário de fato clicou no desenho, realizou uma ação. E se ele realizou uma nova ação com as ferramentas, o canvas não está mais limpo, então custom["limpo"] é setado para false

Este processo ocorre no método setup, e recebe o dicionário custom como parâmetro.

Seguidamente chamamos o método iniciar. Esse método aplica as mudanças de cor, tamanho do lápis, tipo de borda de linha e etc... Ao canvas, ou seja, a partir desse momento, o canvas será desenhado utilizando as configurações específicas daquela ferramenta, também aplica um ponto único no caso de algumas ferramentas.

                    
                        tools[current_tool].setup(custom);
                        tools[current_tool].iniciar(ctx, e);
                    
                

Então, o desenho, ou seja, a aplicação será executada no canvas todas vez que o mouse (ainda pressionado) se mover pelo mesmo, ou apenas durante um clique (mousedown) (LEMBRANDO QUE ESSE EVENTO ESTÁ DENTRO DO ESCOPO DO $(canvas).on('mousedown'...):

                    
                        $(canvas).on('mousemove mousedown', function(e) {
                            tools[current_tool].aplicar(ctx, e);
                        });
                    });
                    
                

O ctx é passado para que o canvas receba as mudanças, e "e" do evento é usado para recuperar as coordenadas do mouse.

Final do movimento (mouseup)

Finalmente, quando o usuário deixar de pressionar o botão esquerdo, o movimento do desenho terá sido concluído, e o evento $(canvas).on('mousemove')... deve ser desativado.

                    
                        $(canvas).on('mouseup mouseleave', function(e) {
                            $(canvas).off('mousemove');
                            simpleDo.pushAction(ctx.getImageData(0, 0, canvas.width, canvas.height));
                        });
                    
                

Toda vez que o mouse for pressionado no canvas, o evento de aplicar a ferramenta será acionado novamente, e assim é mantido o controle do usuário para desenhar somente quando o mouse estiver pressionado em cima do canvas.

Também é ocorre se o mouse deixar o canvas, devido ao 'mouseleave'.

Como isso indica um encerramento de aplicação da ferramenta, empurramos aquele novo estado do canvas para o stack do simpleDo. Acumulando o que foi feito no canvas para fazer e refazer. O atributo simpleDo.action é o responsável de garantir que quando o usuário simplesmente passe o mouse em cima do mouse e retire, dando um trigger nesse evento aquele novo estado, em que nada foi modificado no canvas, não seja adicionado ao stack do simpleDo.

Classes de Ferramentas

As classes de ferramentas são escritas em um documento separado, tool.js, que é importado no ínicio do carregamento da página. Para evitar repetições, qualquer descrição ausente no método de uma classe, estará presente na classe anterior ou na superclasse.

Tool

                    
                        class Tool {

                            constructor(size = 1, color = "#000") {
                                this.brush_size = size;
                                this.brush_color = color;
                            }

                            setup(custom) {
                                this.brush_size = custom["brush_size"];
                                this.brush_color = custom["brush_color"];
                            }

                            iniciar() {
                                throw new Error("Método 'iniciar' deve ser aplicado em subclasses");
                            }

                            aplicar() {
                                throw new Error("Método 'aplicar' deve ser aplicado em subclasses");
                            }

                        }
                    
                

A classe Tool é uma classe abstrata que funciona como base para as demais classes. Toda subclasse derivada desta terá os 4 seguintes métodos, que farão:

Lápis

                    
                        class Pencil extends Tool {

                            iniciar(ctx, e) {
                                ctx.strokeStyle = this.brush_color;
                                ctx.lineWidth = this.brush_size;
                                ctx.lineCap = 'round'; //Faz a linha ficar redonda
                                ctx.lineJoin = 'round'; //Remove os spikes
                                
                                ctx.moveTo(e.offsetX, e.offsetY);
                                ctx.lineTo(e.offsetX, e.offsetY);
                                ctx.stroke();
                            }

                            aplicar(ctx, e) {

                                ctx.lineTo(e.offsetX, e.offsetY);
                                ctx.stroke();
                            }
                        }
                    
                

O lápis é a ferramenta mais simples, funciona com chamadas sucessivas do método lineTo do ctx. Criando a ilusão de um lápis sendo utilizado na tela. A seguir uma explicação dos métodos. Obs: O método construtor e setup são omitidos pois são herdados dos super classe Tool. Os demais métodos tem polimorfismo.

Perceba que os valores do this.brush_color e this.brush_size já foram chamados no método setup herdado da superclasse.

Borracha

                    
                        class Eraser extends Tool {

                            constructor(size = 1, color = "#fff") {
                                super(size, color);
                            }


                            setup(custom) {
                                this.brush_size = custom["brush_size"];
                            }

                            iniciar(ctx, e) {
                                ctx.strokeStyle = this.brush_color;
                                ctx.lineWidth = this.brush_size * 3;
                                ctx.lineCap = 'round'; //Faz a linha ficar redonda
                                ctx.lineJoin = 'round'; //Remove os spikes
                                
                                ctx.moveTo(e.offsetX, e.offsetY);
                                ctx.lineTo(e.offsetX, e.offsetY);
                                ctx.stroke();
                            }

                            aplicar(ctx, e) {

                                ctx.lineTo(e.offsetX, e.offsetY);
                                ctx.stroke();
                            }
                        }
                    
                

A borracha se comporta como o lápis, entretanto tem diferenças importantes.

Sliding Tools

                    
                        class slidingTools extends Tool {
    
                            setup(custom) {
                                    this.brush_size = custom["brush_size"];
                                    this.brush_color = custom["brush_color"];
                                    
                                    //Para armazenar a posição do mouse, onde é o começo do quadrado
                                    this.prevX = custom["prevX"];
                                    this.prevY = custom["prevY"];
                                    
                                    //Para armazenar a imagem e impedir que o desenho fique com um retângulo borrando
                                    this.snapshot = custom["snapshot"];
                                }
                            
                        }
                    
                

As ferramentas Retângulo, Elipse e Linha tem uma necessidade especial. Enquanto o usuário desliza e não solta o botão esquerdo do mouse, deve ser possível enxergar a forma da ferramenta deslizando pelo canvas, mas, somente ser desenhada de fato quando o usuário soltar o botão esquerdo do mouse. A classe abstrata slidingTools funciona para propagar os dados necessários para essa característica em comum nas subclasses geradas a partir dela.

Retângulo

                    
                        class Rectangle extends slidingTools {
    
                                iniciar(ctx) {
                                    ctx.strokeStyle = this.brush_color;
                                    ctx.lineWidth = this.brush_size;
                                    
                                    ctx.lineCap = 'butt'; //Faz as extremidades ficarem pontudas de novo
                                    ctx.lineJoin = 'miter';
                                }
                                
                                aplicar(ctx, e) {
                                    
                                    ctx.putImageData(this.snapshot, 0, 0);
                                    ctx.strokeRect(e.offsetX, e.offsetY, this.prevX - e.offsetX, this.prevY - e.offsetY);
                                }
                        }
                    
                

O retângulo é a forma mais direta. Com poucas diferenças em relação as demais.

Retângulo cheio

                    
                        class RectangleFill extends Rectangle {
    
                            iniciar(ctx) {
                                ctx.strokeStyle = this.brush_color;
                                ctx.fillStyle = this.brush_color;
                                ctx.lineWidth = this.brush_size;
                                
                                ctx.lineCap = 'butt';
                                ctx.lineJoin = 'miter';
                            }
                            
                            aplicar (ctx, e) {
                                ctx.putImageData(this.snapshot, 0, 0);
                                ctx.fillRect(e.offsetX, e.offsetY, this.prevX - e.offsetX, this.prevY - e.offsetY);
                            }
                        }
                    
                

Elipse

                    
                        class Ellipse extends slidingTools {
    
                            iniciar(ctx) {
                                ctx.strokeStyle = this.brush_color;
                                ctx.lineWidth = this.brush_size;
                                
                            }
                            
                            aplicar(ctx, e) {
                                ctx.beginPath();
                                ctx.putImageData(this.snapshot, 0, 0);
                                
                                let radiusx = Math.abs(parseInt((e.offsetX - this.prevX)/2));
                                let radiusy = Math.abs(parseInt((e.offsetY - this.prevY)/2));
                                let x = parseInt((e.offsetX - this.prevX)/2) + this.prevX;
                                let y = parseInt((e.offsetY - this.prevY)/2) + this.prevY;
                                
                                
                                ctx.ellipse(x, y, radiusx, radiusy, 0, 0, Math.PI * 2);
                                ctx.closePath();
                                ctx.stroke();
                            }
                            
                        }
                    
                

A elipse possui certas pelicularidades em relação ao retângulo, principalmente no que diz respeito ao closePath e beginPath, e a ausência das linhas que definem a curvatura das bordas, pois como trata-se de um arco não haverá linhas pontudas e arestas naturalmente.

Para termos certeza que a elipse foi fechada precisamos utilizar o método closePath no final, entretanto, ao chamar esse método se o método beginPath não for chamado novamente não ocorerá um novo stroke no canvas, pois o caminho foi encerrado, e o ctx não sabe onde começar. Por isso é chamado o beginPath nesse caso.

Elipse Cheia

                    
                        class EllipseFill extends Ellipse {
    
                            iniciar(ctx) {
                                ctx.strokeStyle = this.brush_color;
                                ctx.fillStyle = this.brush_color;
                                ctx.lineWidth = 1;
                                                        
                            }
                            
                            aplicar(ctx, e) {
                                ctx.beginPath();
                                ctx.putImageData(this.snapshot, 0, 0);
                                
                                let radiusx = Math.abs(parseInt((e.offsetX - this.prevX)/2));
                                let radiusy = Math.abs(parseInt((e.offsetY - this.prevY)/2));
                                let x = parseInt((e.offsetX - this.prevX)/2) + this.prevX;
                                let y = parseInt((e.offsetY - this.prevY)/2) + this.prevY;
                                
                                
                                ctx.ellipse(x, y, radiusx, radiusy, 0, 0, Math.PI * 2);
                                ctx.closePath();
                                ctx.fill();
                                ctx.stroke();
                            }
                            
                        }
                    
                

Poucos detalhes diferenciam a Elipse Cheia da Elipse vazada, mas são detalhes importantes.

Linha

                    
                        class Line extends slidingTools {
    
    
                            iniciar(ctx) {
                                ctx.strokeStyle = this.brush_color;
                                ctx.lineWidth = this.brush_size;
                                ctx.lineCap = 'round';
                                ctx.lineJoin = 'round';
                            }
                            
                            
                            aplicar(ctx, e) {
                                ctx.beginPath();
                                ctx.putImageData(this.snapshot, 0, 0);

                                ctx.moveTo(this.prevX, this.prevY); //Ponto de origem é fixo
                                ctx.lineTo(e.offsetX, e.offsetY);
                                ctx.stroke();
                            }
                            
                            
                        }
                    
                

No caso da linha, retornamos com a convenção de linhas com pontas arredondadas. Queremos também a ilusão de algo deslizando pelo canvas.

Balde de Preenchimento

                    
                        
                        class Bucket extends Tool {

                            //A RESOLUÇÃO 1080 E 720 ESTÁ HARD 'CODADA' NA FUNÇÃO DO WILLIAM MALONE
                            
                            setup(custom) {
                                this.brush_size = 1; // O tamanho da ferramenta é sempre um pixel independente do que aconteça
                                this.brush_color = custom["brush_color"];
                                this.canvas_size = custom["canvas_size"];
                            }
                            
                            hexToRgb(hex) {
                                // Remover "#" se presente
                                hex = hex.replace(/^#/, '');

                                // Converter para valores de cor RGB
                                var bigint = parseInt(hex, 16);
                                var r = (bigint >> 16) & 255;
                                var g = (bigint >> 8) & 255;
                                var b = bigint & 255;

                                // Retornar valores de cor RGB
                                return { r: r, g: g, b: b };
                            }
                            

                            matchStartColor(pixelPos) {
                                let r = this.colorLayer.data[pixelPos];
                                let g = this.colorLayer.data[pixelPos + 1];
                                let b = this.colorLayer.data[pixelPos + 2];
                                return r === this.startR && g === this.startG && b === this.startB;
                            }

                            colorPixel(pixelPos, selected_color) {
                                this.colorLayer.data[pixelPos] = selected_color.r;
                                this.colorLayer.data[pixelPos + 1] = selected_color.g;
                                this.colorLayer.data[pixelPos + 2] = selected_color.b;
                                this.colorLayer.data[pixelPos + 3] = 255;
                            }

                            //William Malone Algorithm
                            floodFill(startX, startY, selected_color, ctx,
                                newPos, x, y, pixelPos, reachLeft, reachRight) {

                                    newPos = this.pixelStack.pop();
                                    x = newPos[0];
                                    y = newPos[1];
                                    //get current pixel position
                                    pixelPos = (y * 1080 + x) * 4;
                                    // Go up as long as the color matches and are inside the canvas
                                    while (y >= 0 && this.matchStartColor(pixelPos, selected_color)) {
                                      y--;
                                      pixelPos -= 1080 * 4;
                                    }
                                    //Don't overextend
                                    pixelPos += 1080 * 4;
                                    y++;
                                    reachLeft = false;
                                    reachRight = false;
                                    // Go down as long as the color matches and in inside the canvas
                                    while (y < 720 && this.matchStartColor(pixelPos, selected_color)) {
                                      this.colorPixel(pixelPos, selected_color);
                                      if (x > 0) {
                                        if (this.matchStartColor(pixelPos - 4, selected_color)) {
                                          if (!reachLeft) {
                                            //Add pixel to stack
                                            this.pixelStack.push([x - 1, y]);
                                            reachLeft = true;
                                          }
                                        } else if (reachLeft) {
                                          reachLeft = false;
                                        }
                                      }
                                      if (x < 1080 - 1) {
                                        if (this.matchStartColor(pixelPos + 4, selected_color)) {
                                          if (!reachRight) {
                                            //Add pixel to stack
                                            this.pixelStack.push([x + 1, y]);
                                            reachRight = true;
                                          }
                                        } else if (reachRight) {
                                          reachRight = false;
                                        }
                                      }
                                      y++;
                                      pixelPos += 1080 * 4;
                                    }
                                    //recursive until no more pixels to change
                                    if (this.pixelStack.length) {
                                      this.floodFill(startX, startY, selected_color, ctx,
                                        newPos, x, y, pixelPos, reachLeft, reachRight);
                                    }
                            }

                            actionFill(startX, startY, selected_color, ctx) {

                                this.colorLayer = ctx.getImageData(
                                    0,
                                    0,
                                    1080,
                                    720
                                );

                                let startPos = (startY * 1080 + startX) * 4;

                                //Acredito que pega a cor específica
                                this.startR = this.colorLayer.data[startPos];
                                this.startG = this.colorLayer.data[startPos + 1];
                                this.startB = this.colorLayer.data[startPos + 2];

                                if (selected_color.r === this.startR &&
                                    selected_color.g === this.startG &&
                                    selected_color.b === this.startB) return;

                                this.pixelStack = [[startX,startY]];
                                let newPos, x, y, pixelPos, reachLeft, reachRight;

                                this.floodFill(startX, startY, selected_color, ctx,
                                                newPos, x, y, pixelPos, reachLeft, reachRight);
                                
                                ctx.putImageData(this.colorLayer, 0, 0);
                                

                            }
                          
                                
                            // Aqui de fato é onde a mágica acontece, a aplicação da ferramenta em um clique único
                            iniciar(ctx, e) {

                                let selected_color = this.hexToRgb(this.brush_color); //Cor selecionada mas em formato RGB
                                this.actionFill(e.offsetX, e.offsetY, selected_color, ctx);
                                
                                
                            }
                            
                            aplicar(ctx, e) { return; }
                            
                        }
                        
                    
                

Ao contrário das outras ferramentas vamos analisar esta seguindo a ordem lógica que seria aplicada no código.

Ações (Desfazer, Refazer e Limpar Desenho)

SimpleDo

SimpleDo é uma classe que gerencia as duas ações principais no desenho: Desfazer (undo) e Refazer (redo). A classe fica armazenada no documento actions.js e é chamada no documento html. Funciona a partir de armazenamento da imagem, estado do canvas a cada clique nele que o usuário realizar. Essa imagens ficam armazenadas em um stack, a qual retornar vai andando para a esquerda puxando as imagens (ações) mais antigas, e se andar para direita as mais novas. Isso tudo com um limite, se o stack atingir o limite máximo de tamanho definido, o estado mais antigo é removido e o mais novo é inserido naturalmente.

                    
                        class SimpleDo {
                            //Classe abstrata para Redo e Undo, mas só permite um voltar e um avançar
                            
                            //Imagina fazer uma ferramenta de voltar e ir que desse para eu navegar pelas várias partes do desenho, e ir apagando o que eu fiz?
                            //Tipo uma árvorem em que os novos galhos são retornos a uma posição original, na qual uma nova ação diferente é feita, e gera uma ramificação
                            
                            
                            constructor(maxsize, canvasState) {
                                
                                this.maxsize = maxsize - 1; //Tamanho máximo do stack
                                this.stack = [canvasState];
                                
                                this.stackPos = 0; //Onde estou no stack, inicialmente se refere ao tamanho zero
                                this.action = false;
                            }
                            
                            
                            
                            
                            
                            undo (ctx) { //Conforme vou clicando eu vou voltando
                                
                                this.stackPos -= 1; //Quando eu clicar, vou avaliar se estou indo para uma posição válida no stack
                                
                                if (this.stackPos >= 0) {
                                   
                                   ctx.putImageData(this.stack[this.stackPos], 0, 0);
                                   //Habilito a função refazer de novo
                                    
                                }
                                else {
                                    
                                    this.stackPos += 1; //Se não der certo, eu retorno para a posição que estava antes
                                    //return false; //E o botão Desfazer fica desabilitado
                                
                                }
                                
                            }
                            
                            
                            
                            
                            
                            
                            redo (ctx) {
                                
                                this.stackPos += 1; //Quando eu clicar, vou avaliar se estou indo para uma posição válida no stack
                                
                                if (this.stackPos < this.stack.length) {
                                   
                                   ctx.putImageData(this.stack[this.stackPos], 0, 0);
                                   //Habilito a função desfazer de novo
                                    
                                }
                                else {
                                    
                                    this.stackPos -= 1; //Se não der certo, eu retorno para a posição que estava antes
                                    //return false; //E o botão Refazer fica desabilitado
                                
                                }
                                
                            }
                            
                            
                            
                            
                            
                            
                            
                            pushAction (canvasState) { //Uma ação foi feita no canvas
                                
                                if (!this.action) return;
                                //O mais um é pq o splice começa remover incluindo a própria posição
                                this.stack.splice(this.stackPos + 1); //Isso aqui tem que vir primeiro, pois podemos estar iniciando uma nova ação do meio do stack, então removeremos as ações anteriores a partir dali
                                //Para que a útima seja mesmo a última que fizemos
                                
                                //Se o stack atingir o tamanho máximo, eu apago a ação mais antiga
                                if (this.stack.length > this.maxsize) this.stack.shift();
                                
                                //Coloco a ação mais nova
                                this.stack.push(canvasState);
                                
                                //Removo o resto do array se tiver, a partir da posição onde estou
                                
                                //Vou me movimentando junto com o array, mas não extrapolo o limite
                                if(this.stackPos < this.maxsize) this.stackPos += 1;
                                
                                this.action = false; //Retorna a variável para false, para esperar um clique no canvas para que seja true de novo.
                                
                            }
                            
                        }