Has anyone managed to force the canvas that compos...
# compose-web
b
Has anyone managed to force the canvas that compose draws to be transparent such that you could layer an html/native view behind/underneath the canvas?
m
Have you tried drawWithContent already?
b
It's not something I can try to any effect. I've tried doing basic html experiments to confirm it's possible to layer canvas contents over html contents, eg
Copy code
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Overlay Canvas on Div</title>
  <style>
      #container {
          position: relative;
          width: 500px;
          height: 500px;
      }

      .overlay-div {
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          background-color: rgba(255, 0, 0, 0.5); /* Semi-transparent red */
          z-index: 0; /* Ensure the div is below the canvas */
      }

      .canvas {
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          z-index: 1; /* Ensure the canvas is above the div */
      }
  </style>
</head>
<body>
<h1>Overlay Canvas on Div</h1>
<div id="container">
  <div class="overlay-div"></div>
  <canvas id="canvas2" class="canvas"></canvas>
</div>

<script>
   // Canvas dimensions
   const width = 500;
   const height = 500;

   // Get canvas element and set its size
   const canvas2 = document.getElementById('canvas2');
   canvas2.width = width;
   canvas2.height = height;

   // Get the 2D drawing context
   const ctx2 = canvas2.getContext('2d');

   // Draw a blue circle on the canvas
   ctx2.fillStyle = 'blue';
   ctx2.globalAlpha = 0.8; // Set transparency for better visual overlap
   ctx2.beginPath();
   ctx2.arc(250, 250, 100, 0, Math.PI * 2);
   ctx2.fill();
</script>
</body>
</html>
I've also tried changing the context for the canvas that Compose draws to to enable alpha/transparency, but all of my experiments result in either the compose window forcing a full background color over the html layer or the html layer being fully visible, and the compose window not being displayed
I think I'd have to dig into the compose/skia rendering code to understand why this is the case. I assume there is probably a good reason, otherwise we'd already an HtmlView in wasmJs w/o caveats as we have AndroidView and NativeView on android + ios
Last night, I realized I can just restructure my UX to avoid overlap issues, and some of the changes are actually improvements. So that's one route for working around layering issues creatively
This morning I had one more idea that I am surprised I didn't think of trying before! I can also try out having two Compose windows such that the stack is
Copy code
<div id="composeViewport1" z-index=1></div>
<div id="htmlViewContents" z-index=2></div>
<div id="composeViewport3" z-index=3></div>
Would be very hacky, but in theory this could also work.
m
Sorry, I misunderstood your question.
s
I’m very interested in this as well for my work on MapLibre Compose. Do you have a functioning implementation of an HtmlView? Did your idea of stacking another compose window on top of the html view work out to composite them properly?
b
@Sargun Vohra I copied and pasted the two files from https://github.com/Hamamas/Kotlin-Wasm-Html-Interop into my project
It has the caveat that you cannot render anything from your canvas above the map
Will probably take a stab later this week on stacking another compose window above the htmlview.
s
I wrote myself a small
HtmlElement
compose library that does the same thing as that one: place the interop html element above the Compose canvas: https://github.com/sargunv/maplibre-compose/tree/main/lib/compose-html-interop Filed https://github.com/sargunv/maplibre-compose/issues/210 to track the compositing issue with my project. I wonder if it’s possible to apply some alpha mask to composite things properly? I think that’s how Android views interop with SurfaceView, for example (SurfaceView is always behind Android views, but a hole in punched in the UI to see the Surface where it should be visible)
(my HTML interop library will be on maven central alongside the next release of MapLibre Compose in the next few days)
Update: I’ve figured out forcing the Compose canvas to be transparent, see https://github.com/sargunv/maplibre-compose/pull/215/files
Copy code
modifier.drawBehind {
  drawRect(
    color = Color.Transparent,
    size = size,
    blendMode = BlendMode.Clear
  )
}
Compose is still capturing touch/mouse input, so it’s not being passed through to the map underneath. But otherwise, it works. See these screenshots; the first shows map controls from Compose rendered on the corners of the
HtmlElement
map. The second shows a Compose AlertDialog on top of the
HtmlElement
map.
💯 1
m
So, you are basically back to what I originally proposed 😉.
drawBehind
is very similar to
drawWithContent
.
💡 1
b
Brilliant @Sargun Vohra -- thank you sir 👏🏻 screenshot of my wasm app rendered with mobile dimensions
🎉 1
s
here’s my little library for that: https://kotlinlang.slack.com/archives/C0BJ0GTE2/p1735697631791299. You can pass
zIndex = "-1"
to the composable to render behind. if someone figures out a fix for the pointer input issue when rendering below, please PR or file an issue!
👏 1
b
I took a stab at passing through synthetic events, but I was only able to see double click gesture get recognized by mapbox w/ that approach
Copy code
<script>
   window.startRelaying = function () {
      // 1) Grab references to the canvas & the target div
      const canvas = document.getElementById("canvasHolder");
      const mapContainer = document.getElementsByClassName("mapboxgl-canvas")[0];

      if (!canvas) {
         console.error('canvas is undefined');
      }

      if (!mapContainer) {
         console.error("mapContainer is undefined");
      }

      const eventTypes = [
         "pointerdown",
         "pointermove",
         "pointerup",
         "pointercancel",
         "pointerover",
         "pointerout",
         "pointerenter",
         "pointerleave",
         "gotpointercapture",
         "lostpointercapture",

         "mousedown",
         "mousemove",
         "mouseup",
         "mouseover",
         "mouseout",
         "mouseenter",
         "mouseleave",
         "click",
         "dblclick",
         "contextmenu",
         "wheel",

         // Touch Events
         "touchstart",
         "touchmove",
         "touchend",
         "touchcancel",
      ];

      function relayEvent(originalEvent) {
         console.log(
            "Relaying pointer event:",
            originalEvent.type,
            "pointerId:", originalEvent.pointerId,
            "clientX:", originalEvent.clientX,
            "clientY:", originalEvent.clientY
         );

         const rectCanvas = canvas.getBoundingClientRect();
         const rectMap = mapContainer.getBoundingClientRect();

         const offsetX = originalEvent.clientX - rectCanvas.left;
         const offsetY = originalEvent.clientY - <http://rectCanvas.top|rectCanvas.top>;

         const mapX = rectMap.left + offsetX;
         const mapY = <http://rectMap.top|rectMap.top> + offsetY;

         const syntheticEvent = new PointerEvent(originalEvent.type, {
            ...originalEvent,
            clientX: mapX,
            clientY: mapY,
            bubbles: true,
            cancelable: true,
            composed: true,
         });

         mapContainer.dispatchEvent(syntheticEvent);
      }

      eventTypes.forEach(type => {
         canvas.addEventListener(type, relayEvent);
      });
   }
</script>
s
For maps in particular, here’s a proof of concept of passing through map gestures by reimplementing them with Compose’s
transformable
. The PoC only has panning, but zoom and rotate should be possible similarly
💡 1