Search code examples
rshinyshinyjs

How to draw and erase and change the brush size and color?


Colors should appear and change when dragging the slider button but nothing happens, there is just a blank canvas in the right hand side of the screen. I changed some things before it was not even a canvas on the right hand side of the screen but now when there is a canvas nothing happens when I use slider option like color should appear. When I select a color from the dropdown and use slider then the color should also change:

# install.packages(c("shiny", "shinyjs"))

library(shiny)
library(shinyjs)

ui <- fluidPage(
  useShinyjs(),
  titlePanel("Drawing App"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("brushSize", "Brush Size", min = 1, max = 20, value = 5),
      selectInput("brushColor", "Brush Color",
                  choices = c("Black", "Red", "Blue", "Green", "Eraser"),
                  selected = "Black")
    ),
    mainPanel(
      div(id = "canvas", style = "border:1px solid #000; height: 400px;"),
      tags$script('
        var isDrawing = false;
        var context;

        $(document).ready(function(){
          var canvas = document.getElementById("canvas");
          context = canvas.getContext("2d");

          $("#canvas").mousedown(function(e){
            isDrawing = true;
            context.beginPath();
            context.moveTo(e.pageX - canvas.offsetLeft, e.pageY - canvas.offsetTop);
          });

          $("#canvas").mousemove(function(e){
            if(isDrawing){
              var x = e.pageX - canvas.offsetLeft;
              var y = e.pageY - canvas.offsetTop;
              var color = $("#brushColor").val();
              var size = $("#brushSize").val();
              context.lineWidth = size;
              if(color === "Eraser"){
                context.strokeStyle = "#FFF"; // White color for eraser
              } else {
                context.strokeStyle = color.toLowerCase();
              }
              context.lineTo(x, y);
              context.stroke();
            }
          });

          $("#canvas").mouseup(function(){
            isDrawing = false;
          });

          $("#canvas").mouseleave(function(){
            isDrawing = false;
          });

          Shiny.setInputValue("canvasData", canvas.toDataURL());
        });

        Shiny.addCustomMessageHandler("clearCanvas", function(message) {
          context.clearRect(0, 0, canvas.width, canvas.height);
        });
      ')
    )
  )
)

server <- function(input, output, session) {
  observe({
    brush_color <- switch(input$brushColor,
                          "Black" = "black",
                          "Red" = "red",
                          "Blue" = "blue",
                          "Green" = "green",
                          "Eraser" = "white")

    shinyjs::runjs(paste0('context.strokeStyle = "', brush_color, '";'))
  })

  observeEvent(input$canvasData, {
    session$sendCustomMessage("clearCanvas", NULL)
    shinyjs::runjs(paste0('var img = new Image(); img.src = "', input$canvasData, '"; context.drawImage(img, 0, 0);'))
  })
}

shinyApp(ui, server)

Solution

  • Note that:

    • You can't use getContext() on a div, you have to create your canvas using tags$canvas(height="400px", id="canvas", style="border: 1px solid #000;").
    • One usually needs $(document).on("shiny:connected", otherwise shiny may not be ready when you try to use setInputValue.
    • The canvas attributes for width and height should be set up correctly: canvas.setAttribute("width", canvas.parentNode.offsetWidth); canvas.setAttribute("height", canvas.parentNode.offsetHeight);
    • We need the bounds var bounds = canvas.getBoundingClientRect(); and access them (e.g. context.moveTo(e.pageX - bounds.left, e.pageY - bounds.top);), otherwise the cursor position won't match.

    Then it will work.

    enter image description here

    library(shiny)
    library(shinyjs)
    
    ui <- fluidPage(
        useShinyjs(),
        titlePanel("Drawing App"),
        sidebarLayout(
            sidebarPanel(
                sliderInput("brushSize", "Brush Size", min = 1, max = 20, value = 5),
                selectInput("brushColor", "Brush Color",
                            choices = c("Black", "Red", "Blue", "Green", "Eraser"),
                            selected = "Black")
            ),
            mainPanel(
                tags$canvas(
                    height="400px", id="canvas", style="border: 1px solid #000;"
                ),
                tags$script(HTML('
            var isDrawing = false;
            var context;
    
            $(document).on("shiny:connected", function(){
              var canvas = document.getElementById("canvas");
              canvas.setAttribute("width", canvas.parentNode.offsetWidth);
              canvas.setAttribute("height", canvas.parentNode.offsetHeight);
              context = canvas.getContext("2d");
              var bounds = canvas.getBoundingClientRect();
    
              $("#canvas").mousedown(function(e){
                isDrawing = true;
                context.beginPath();
                context.moveTo(e.pageX - bounds.left, e.pageY - bounds.top);
              });
    
              $("#canvas").mousemove(function(e){
                if(isDrawing){
                  var x = e.pageX - bounds.left;
                  var y = e.pageY - bounds.top;
                  var color = $("#brushColor").val();
                  var size = $("#brushSize").val();
                  context.lineWidth = size;
                  if(color === "Eraser"){
                    context.strokeStyle = "#FFF"; // White color for eraser
                  } else {
                    context.strokeStyle = color.toLowerCase();
                  }
                  context.lineTo(x, y);
                  context.stroke();
                }
              });
    
              $("#canvas").mouseup(function(){
                isDrawing = false;
              });
    
              $("#canvas").mouseleave(function(){
                isDrawing = false;
              });
    
              Shiny.setInputValue("canvasData", canvas.toDataURL());
            });
    
            Shiny.addCustomMessageHandler("clearCanvas", function(message) {
              context.clearRect(0, 0, canvas.width, canvas.height);
            });
          '))
            )
        )
    )
    
    server <- function(input, output, session) {
        observe({
            brush_color <- switch(input$brushColor,
                                  "Black" = "black",
                                  "Red" = "red",
                                  "Blue" = "blue",
                                  "Green" = "green",
                                  "Eraser" = "white")
            
            shinyjs::runjs(paste0('context.strokeStyle = "', brush_color, '";'))
        })
        
        observeEvent(input$canvasData, {
            session$sendCustomMessage("clearCanvas", NULL)
            shinyjs::runjs(paste0('var img = new Image(); img.src = "', input$canvasData, '"; context.drawImage(img, 0, 0);'))
        })
    }
    
    shinyApp(ui, server)