konvajs / konva

Konva.js is an HTML5 Canvas JavaScript framework that extends the 2d context by enabling canvas interactivity for desktop and mobile applications.
http://konvajs.org/
Other
11.59k stars 926 forks source link

Capturing what the what is rendered to a layer within a region of interest using the _context? #1822

Closed ArEnSc closed 1 month ago

ArEnSc commented 1 month ago

I was using this where # captureCanvas: Konva.Rect;

const dataURL = this.videoLayer.toDataURL({ x: this.captureCanvas.x(), y: this.captureCanvas.y(), width: this.captureCanvas.width(), height: this.captureCanvas.height(), });

to capture what was in the a konva ImageRect (this.captureCanvas) It works however is super slow.

I need to get direct access to the data in that region in the layer. But using the offScreenCanvas and grabbing the 2D context.

So I was able to set that up, however... when performing this operation

 this.offScreenCanvas.width = this.width;
        this.offScreenCanvas.height = this.height;
        this.context.drawImage(
          this.videoLayer.canvas._canvas,
          this.positionX,
          this.positionY,
          this.captureCanvas.width(),
          this.captureCanvas.height(),
          0,
          0,
          this.width,
          this.height,
        );

It doesn't seem grab the region correctly either due to scaling issues? when I zoom in and out of the browser a bit, it seems to work but then on different displays it doesn't capture it correctly. I suspect this has to do with pixel ratio? I need it to be rendered on the offScreen Canvas so I can send this off to a web worker to process @lavrton lmk if if you have any other ideas of how to achieve this.

lavrton commented 1 month ago

Make a small demo. I don't understand what you are trying to do.

ArEnSc commented 1 month ago
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, user-scalable=no"
    />
    <title>Konva App</title>
    <script src="https://cdn.jsdelivr.net/npm/konva@8.3.5/konva.min.js"></script>
    <style>
      body {
        margin: 0;
        padding: 0;
        overflow: hidden;
        width: 100vw;
        height: 100vh;
      }
      #container {
        width: 100%;
        height: 100%;
      }
      #captureButton {
        position: absolute;
        top: 10px;
        left: 10px;
        padding: 10px 20px;
        background-color: #007bff;
        color: white;
        border: none;
        border-radius: 5px;
        cursor: pointer;
      }
      #captureButton:hover {
        background-color: #0056b3;
      }
    </style>
  </head>
  <body>
    <div id="container"></div>
    <button id="captureButton">Capture Canvas</button>
    <script>
      // mobile screen resolution
      // create off screen canvas to draw to
      const width = 720
      const height = 1280
      const offScreenCanvas = new OffscreenCanvas(width, height)
      const context = offScreenCanvas.getContext("2d")

      // Create a Konva Stage
      const stage = new Konva.Stage({
        container: "container", // ID of the container element
        width: window.innerWidth,
        height: window.innerHeight,
      })

      // Create a Konva Layer
      const layer = new Konva.Layer()
      stage.add(layer)

      // Create a rectangle with dimensions 720 by 1280
      const rect = new Konva.Rect({
        x: (window.innerWidth - width) / 2, // Center the rectangle horizontally
        y: (window.innerHeight - height) / 2, // Center the rectangle vertically
        width: width,
        height: height,
        fill: "red", // Fill color
        stroke: "green", // Border color
        strokeWidth: 5, // Border width
      })

      // Add the rectangle to the layer
      layer.add(rect)

      // Function to create a star
      function createStar(x, y) {
        return new Konva.Star({
          x: x,
          y: y,
          numPoints: 5,
          innerRadius: 20,
          outerRadius: 40,
          fill: "yellow",
          stroke: "black",
          strokeWidth: 2,
        })
      }

      // Calculate positions for the stars
      const rectX = rect.x()
      const rectY = rect.y()
      const rectWidth = rect.width()
      const rectHeight = rect.height()

      const positions = [
        { x: rectX, y: rectY }, // Top-left corner
        { x: rectX + rectWidth, y: rectY }, // Top-right corner
        { x: rectX, y: rectY + rectHeight }, // Bottom-left corner
        { x: rectX + rectWidth, y: rectY + rectHeight }, // Bottom-right corner
        { x: rectX + rectWidth / 2, y: rectY + rectHeight / 2 }, // Center
      ]

      // Add stars to the specified positions
      positions.forEach((pos) => {
        const star = createStar(pos.x, pos.y)
        layer.add(star)
      })

      // Draw the layer
      layer.draw()

      // Define an async function to capture the canvas content
      async function captureCanvas() {
        // Capture with the offscreen canvas
        context.drawImage(
          layer.getCanvas()._canvas,
          rectX,
          rectY,
          rectWidth,
          rectHeight,
          0,
          0,
          width,
          height
        )

        // Convert the offscreen canvas to a Blob
        const blob = await offScreenCanvas.convertToBlob({
          quality: 1.0,
          type: "image/jpeg",
        })

        // I want to call this and send this buffer to a shared worker
        // for faster processing.
        //offScreenCanvas.transferToImageBitmap();

        // Create a URL for the Blob and display the image
        const url = URL.createObjectURL(blob)
        const img = new Image()
        img.src = url
        document.body.appendChild(img) // Append the image to the body or any other container

        // Optionally, you can download the captured image
        const link = document.createElement("a")
        link.href = url
        link.download = "snapshot.jpg"
        link.click()
      }

      // Attach event listener to the button
      document
        .getElementById("captureButton")
        .addEventListener("click", captureCanvas)
    </script>
  </body>
</html>

This is a toy example: But I want to be able to capture any Konva items that sit on top of that red layer. It seems to work but when you zoom in and out. It causes issues with things being offset. With it full screen it is a bit offset still. I want to do it this way because I want to be a able to convert the offscreen blob on a shared worker. snapshot (22)

I have taken a look at using dataToURL on the layer, and it captures everything on top of the layer. However its way to slow for my purposes.

So do you know how I would be able to capture this layer and anything that would sit on top of it correctly?

Here's another toy example that also tries to account for resizing of the window it you zoom in and out of the browser window with control + or -. or minimize the window the capture will be incorrect.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, user-scalable=no"
    />
    <title>Konva App</title>
    <script src="https://cdn.jsdelivr.net/npm/konva@8.3.5/konva.min.js"></script>
    <style>
      body {
        margin: 0;
        padding: 0;
        overflow: hidden;
        width: 100vw;
        height: 100vh;
      }
      #container {
        width: 100%;
        height: 100%;
      }
      #captureButton {
        position: absolute;
        top: 10px;
        left: 10px;
        padding: 10px 20px;
        background-color: #007bff;
        color: white;
        border: none;
        border-radius: 5px;
        cursor: pointer;
      }
      #captureButton:hover {
        background-color: #0056b3;
      }
    </style>
  </head>
  <body>
    <div id="container"></div>
    <button id="captureButton">Capture Canvas</button>
    <script>
      // mobile screen resolution
      // create off screen canvas to draw to
      const width = 720
      const height = 1280
      const offScreenCanvas = new OffscreenCanvas(width, height)
      const context = offScreenCanvas.getContext("2d")

      // Create a Konva Stage
      const stage = new Konva.Stage({
        container: "container", // ID of the container element
        width: window.innerWidth,
        height: window.innerHeight,
      })

      // Create a Konva Layer
      const layer = new Konva.Layer()
      stage.add(layer)

      // Create a rectangle with dimensions 720 by 1280
      const rect = new Konva.Rect({
        x: (window.innerWidth - width) / 2, // Center the rectangle horizontally
        y: (window.innerHeight - height) / 2, // Center the rectangle vertically
        width: width,
        height: height,
        fill: "red", // Fill color
        stroke: "green", // Border color
        strokeWidth: 5, // Border width
      })

      // Add the rectangle to the layer
      layer.add(rect)

      // Function to create a star
      function createStar(x, y) {
        return new Konva.Star({
          x: x,
          y: y,
          numPoints: 5,
          innerRadius: 20,
          outerRadius: 40,
          fill: "yellow",
          stroke: "black",
          strokeWidth: 2,
        })
      }

      // Function to calculate positions for the stars
      function calculateStarPositions(rect) {
        const rectX = rect.x()
        const rectY = rect.y()
        const rectWidth = rect.width()
        const rectHeight = rect.height()

        return [
          { x: rectX, y: rectY }, // Top-left corner
          { x: rectX + rectWidth, y: rectY }, // Top-right corner
          { x: rectX, y: rectY + rectHeight }, // Bottom-left corner
          { x: rectX + rectWidth, y: rectY + rectHeight }, // Bottom-right corner
          { x: rectX + rectWidth / 2, y: rectY + rectHeight / 2 }, // Center
        ]
      }

      // Create and add stars to the layer
      let stars = []
      function addStars() {
        const positions = calculateStarPositions(rect)
        positions.forEach((pos) => {
          const star = createStar(pos.x, pos.y)
          stars.push(star)
          layer.add(star)
        })
      }

      addStars()

      // Draw the layer
      layer.draw()

      // Function to reposition the rectangle and stars
      function repositionElements() {
        rect.x((window.innerWidth - width) / 2)
        rect.y((window.innerHeight - height) / 2)

        const positions = calculateStarPositions(rect)
        stars.forEach((star, index) => {
          star.x(positions[index].x)
          star.y(positions[index].y)
        })

        stage.width(window.innerWidth)
        stage.height(window.innerHeight)
        layer.draw()
      }

      // Add event listener for window resize
      window.addEventListener("resize", repositionElements)

      // Define an async function to capture the canvas content
      async function captureCanvas() {
        // Capture with the offscreen canvas
        offScreenCanvas.width = width
        offScreenCanvas.height = height
        context.drawImage(
          layer.getCanvas()._canvas,
          rect.x(),
          rect.y(),
          rect.width(),
          rect.height(),
          0,
          0,
          width,
          height
        )

        // Convert the offscreen canvas to a Blob
        const blob = await offScreenCanvas.convertToBlob({
          quality: 1.0,
          type: "image/jpeg",
        })

        // Create a URL for the Blob and display the image
        const url = URL.createObjectURL(blob)
        const img = new Image()
        img.src = url
        document.body.appendChild(img) // Append the image to the body or any other container

        // Optionally, you can download the captured image
        const link = document.createElement("a")
        link.href = url
        link.download = "snapshot.jpg"
        link.click()
      }

      // Attach event listener to the button
      document
        .getElementById("captureButton")
        .addEventListener("click", captureCanvas)
    </script>
  </body>
</html>
ArEnSc commented 1 month ago

@lavrton thanks for responding!

lavrton commented 1 month ago

I still don't get it.

Why don't you use regular toDataURL() or toCanvas() exports? If it is slow for you, can you give a context when/how you call it?

You may have scale issues, because of pixelRatio. Canvas size of the layer may be bigger than css size, to have sharp images on HDPI devices like retina.

ArEnSc commented 1 month ago

to

Yeah it was the scale issues, I dug into the code and created something that would allow me do what u wanted by subclassing then draw scene