Position HTML div on Entity position

Our goal is to show speech bubbles above the heads of characters. We tried using PlayCanvas’ “Screen” UI system first, but ran into some barriers (most notably the lack of a ‘ContentSizeFitter’ to make the speech bubble auto-expand with its content) and decided to go for a CSS/HTML solution instead.

This tutorial helped us setting the basic data/event binding up, but we’re stuck trying to position/anchor the div at the desired position.

We’re using the following snippet to position the element:

// Add the HTML
this.div = document.createElement('div');
this.div.classList.add('speech_bubble');

//get the camera
var camera = this.app.root.findComponent("camera");

//convert this SpeechBubble's entity position to screen position
var entityPos = this.entity.getPosition();
var screenPos = camera.worldToScreen(entityPos);

//get bounding rect of canvas
var canvas = document.getElementById('application-canvas');
var boundingRect = canvas.getBoundingClientRect();

//get root coordinates of canvas relative to window
var rootX = boundingRect.left + window.scrollX;
var rootY = boundingRect.top + window.scrollY;

//apply offset from canvas' relative position
var finalX = rootX + screenPos.x;
var finalY = rootY + screenPos.y;

//set position
this.div.style.left = pc.string.format("{0}px",finalX);
this.div.style.top = pc.string.format("{0}px",finalY);

Here’s how it looks when playing:

In the screenshot, you can see that the speech bubble div is positioned at (218,172) - those are the numbers that this.entity.getPosition() reported (on a script attached to the small grey sphere). The coordinates that we would like to get from PlayCanvas, however, are, in this case, (1119/485).

We’ve tried getting this to work for quite some time now. We tried creating a more suitable DOM setup by nesting PlayCanvas’ into a top-level

together with another
of the same dimension for the speech bubbles, we tried with and without offsetting it according to the canvas’ BoundingRect, among other things.

The root issue seems to be that the coordinates that camera.worldToScreen(entityPos) returns are not actual DOM or window or screen coordinates, but seem to be specially scaled coordinates for PlayCanvas’ “Screen” UI system. We tried finding documentation on how to convert these to DOM-usable coordinates, but didn’t find any real answer.

Has anyone done this before, and could help with some inputs? Thanks!

Do you have device pixel ratio enabled?

@yaustar thank you for the quick reply! Sorry I forgot to mention this. We tried (and I just tried again to make sure) with and without Device Pixel Ratio enabled, and get the exact same result/positioning.

This might have to do with the fact that I’ve nested the canvas in a div in this latest iteration - I will remove that setup and try again to ensure that’s not the issue.

Edit: I’ve reverted to the basic setup (no modification/nesting of the PlayCanvas <canvas>, and can confirm that enabling/disabling Device Pixel Ratio doesn’t make a difference on my machine - both lead to the same wrong result.

Edit: For better visual cues, I’ve placed one anchor entity in each corner of the camera’s viewport:

This image shows well that at least the coordinate system’s orientation is correct (the four bubbles appear in the correct corners) - it’s just that we seem to be missing some ‘Scaling Factor’ of sorts. If I would multiply the coordinates of every speech bubble here by ~6, they would fit perfectly, for example. This scale number changes with window resolution of course, so I’d need to get it programmatically.

Hi @RobinB,

I gave this a try on the example project and it seems to be working fine:

worldToScreen() will return window coordinates, that take into account the device pixel ratio as well.

Here is my code:

Ui.prototype.update = function () {
  
    this.camera.camera.worldToScreen(this.target.getPosition(), this.vec2);

    var offsetY = 170;
    
    this.div.style.top = (this.vec2.y - this.div.offsetHeight / 2 - offsetY) + 'px';
    this.div.style.left = (this.vec2.x - this.div.offsetWidth / 2) + 'px';
};

Sample project: https://playcanvas.com/editor/scene/969069

1 Like

Hi @Leonidas, thank you so much for the example project! I’ll have to see where I went wrong in my implementation exactly, but your example project helps a great deal! Thanks for putting this together. May I recommend having this added to the official example collection even? I imagine it’s a feature that quite a range of games may want to implement - and having this reference can avoid confusion for other devs :grinning: Thanks!

2 Likes

That’s a good idea, @yaustar may be able to see to it. :wink:

1 Like

To conclude from my side:

I’ve implemented your solution on our project and it works great! The issue on our side was that I ran the positioning once on SpeechBubble.prototype.initialize instead of SpeechBubble.prototype.update. I planned to move this to update later, but first wanted to get it working during initialization. It seems, though, that some part of the required layouting information isn’t initialized yet during initialize, leading to a wrongly scaled coordinate matrix. Simply running the exact same code snippet in update yielded the desired result.

Thanks again, your help is much appreciated!

1 Like

Thanks for sharing your resolution, if you still would like to run it once on the start of the project you may be able to do so in the postInitialize() script method.

That method will be called after all entities have been initialized and are in place.

2 Likes