All Blog Posts  |  Next Post  |  Previous Post

Extend TMS WEB Core with JS Libraries with Andrew:
HexaGongs Part 2 of 5: Interface


Tuesday, June 13, 2023

Photo of Andrew Simard

In the first post of our HexaGongs miniseries, we were focused on creating the background layer for our TMS WEB Core project - a hexagonal grid arrangement. We also added a background animation to make it a little more interesting visually and sorted out how and where to place our main UI buttons. In this post, we're going to work through creating a key part of the interface - placing and arranging the "HexaGong" elements within that same grid structure. We've covered some of this approach recently, in the Moving Elements blog post. This time out, we'll expand on that work, adding support for a cursor of sorts, as well as addressing issues that crop up when the dimensions of the hexagonal grid change.


  1. InteractJS.
  2. Change Mode.
  3. Cursor.
  4. HexaGongs.
  5. Drag and Swap.
  6. Jiggle Mode.
  7. Dimensionally Challenged.
  8. Next Time.


To get started today, we'll need another JavaScript library, the same InteractJS library that we used previously. This library helps simplify a great deal of the drag-and-drop and resizing operations that we'll be using in this project.  The basic idea is that it allows us to simply add HTML classes to the elements we want to interact with (or don't want to interact with, just as importantly), while also giving us the ability to precisely control how these operations work, using JavaScript. To add this to our project, we can use the Manage JavaScript Libraries feature of the Delphi IDE, or add it directly to our Project.html file. And as usual, we can use a CDN for this or just include it in our project directly.  Let's go with the usual CDN approach here.

    <!-- InteractJS -->
    <script src=""></script>

No CSS is required in this case, as it doesn't do anything in terms of styling.

Change Mode.

In our last post, we created the background as a collection of <div> elements, with each element displaying a single hexagon. These are then used as holders or "cups" into which we can add other elements, ensuring that they are positioned properly by being bounded by the hexagon they are added to. During the normal use of our app, we'll just be tapping on them to play whatever audio is linked to that particular "HexaGong". When we want to make changes to the contents or rearrange them on the hexagonal grid, we'll need to enter a "Change Mode" as it is referred to internally. We'll see a little later how this is the same as "jiggle mode" we've used before but as we don't have anything to jiggle just yet, we'll revisit that a bit later.

The icon in the bottom-right corner is what we'll use to toggle this Change Mode, and it works much like the set of buttons we covered last time. In this case, the primary button will enable or disable this mode, and the secondary buttons include "edit", "clone", and "delete" functions. For today, we'll only be concerned with creating new HexaGongs, and just the stubs at that. What is of more immediate concern is where are they going to be created.  We'll need some kind of way to indicate which HexaGong we want to clone or delete, or where we want to drop a new HexaGong. Basically, we need a cursor.


To implement a cursor, we're going to use another TWebButton component and move it around just like we did with the other UI buttons. It will even start out in the divButtons TWebHTMLDiv component, and return there when not needed, or temporarily when the window is resized, so it doesn't get deleted when the hexagons are regenerated.

Within our "cup" hexagon, we'll need to be mindful of what is placed inside, and in what order, so that we don't obscure the contents. But we also have to be mindful of what the order is when it comes time to drag elements around - the topmost element will be selected by default. In the case of the cursor, we'll try and ensure that it stays in the middle (z-index: 5 in this case). To ensure we can still see what is beneath the cursor, we'll use a translucent background. It would also be ideal if anything above the cursor was also translucent, otherwise, the cursor will be obscured. Here's what our CSS for the cursor button looks like.

.CursorButton {
  display: flex;
  clip-path: polygon(25% 5%, 75% 5%, 100% 50%, 75% 95%, 25% 95%, 0% 50%);
  background: radial-gradient(#000000, #FFFF00); /* yellow but mostly transparent */
  border: none;
  opacity: 0.4;
  transition: opacity 1000ms;
.CursorButton:hover {
  opacity: 0.8;

Note that we're using the same clip-path to draw the cursor as a hexagon shape. Much of the time, the cursor will be within a hexagon, so this might seem redundant - the hexagon parent will clip the cursor button just fine all on its own. But as we'll see shortly, sometimes the cursor button will not be nestled inside its parent like this, so we need to configure it the same as if it wasn't contained within a parent hexagon.

There are several ways the cursor can be moved. The most obvious way is by clicking on one of the hexagons in the background. But we didn't add a "click" event handler to those <div> elements. Because we don't need to.  JavaScript automatically uses a "bubble" or "propagation" mechanism. Clicking on an element will trigger a "click" event on that element, but also on all of its parent elements, all the way up the DOM tree. The event object that is generated also indicates what was originally clicked on. So we can instead just add an event handler to the divBackground element and then look and see whether it was a hexagon, and if so, which one. And then move the cursor there.

  // Deal with button clicks that aren't on buttons directly
    divBackground.addEventListener('click', (event) => {
      // Cursor handling
      if ('Valid')
            && (This.ChangeMode == true)
            && !'Button')
            && !'MainButton') ) {
        // Remove jiggle if hexagon is otherwise empty
        if (This.PositionsG[btnCursor.parentElement.getAttribute('position')] == -1) {
        // Move cursor to new position;
        // Set to jiggling (might be jiggling already)'animation-name','jiggle');
      // If not cursor and not primary button and not secondary button, then hide buttons
      else if (! ('Button') ||'MainButton')
        )) {

We'll have to update this later when we have actual HexaGongs to click on while not in Change Mode.4 And there are extra calls to deal with jiggle mode, but we'll get to those shortly. All this is really doing is checking for a "Valid" hexagon click, and then using appendChild() to move our btnCursor TWebButton component directly into that element (the element). If we click on anything else, like the background hexagons around the edges for example, then we disable Change Mode (or really, any mode we were in) and things are reset to the normal non-Change Mode interface. 

If we click on any of the primary or secondary buttons, then we'll ignore it here and expect that those button events will be handled separately. We also have to be careful that we're not clicking on something else within the hexagon (like the cursor itself) that would return a different element than we're expecting. This is largely done by ignoring anything without the "Valid" class.

And one more thing - we're keeping track of the cursor "position" by adding an HTML attribute - "position" of course - to indicate where in the Positions array the cursor is located, where this is set to -1 if the cursor isn't visible. We can then use this value later when we want to know where to drop a new HexaGong or to check and see if one is available for cloning or deletion.

There are other ways to move the cursor, such as moving a HexaGong that is currently highlighted by the cursor, or by dragging the cursor itself around. This functionality will be incorporated directly into the code we'll need for dragging the HexaGongs themselves. So let's get started on that. 


To manage the HexaGong elements, we'll need another set of dynamic arrays - Gongs, GongsP, and PositionsG to start with. Clicking the "edit" button (pencil) while a hexagon is highlighted with our btnCursor will add a new HexaGong in that Position to our set of arrays, or edit an existing one if that Position is already occupied. There are of course many options we'll need to manage - that's the entire focus of the next post in this miniseries. For today, we'll just be creating stubs of a sort. We can create them dynamically, much like we're doing with the animation situation, as TWebHTMLDiv components.

Initially, we'll just add a background color and a number. Structurally, the background <div> elements, our "cups", will contain these new TWebHTMLDiv components. But as with the primary and secondary buttons and the cursor, we'll have to move them out and back in whenever the window size changes, to prevent them from being deleted from the page. Here, we're setting the z-index value to 10 to ensure that they are the top-most element, specifically above the cursor. As these are also translucent, we still get the highlighting effect of the cursor, but the contents are not as obscured as would be the case if the cursor was on top. Also, when we go to drag this element, it is on top instead of the cursor, which is more important when it comes to this choice for z-ordering.

    Gongs: Array of TWebHTMLDiv;    // HexaGong UI elements
    GongsP: Array of Integer;       // Position of HexaGong
    PositionsG: Array of Integer;   // Gong # at a Position

procedure TForm1.btnEditClick(Sender: TObject);
  CursorPosition: Integer;
  CursorPosition := StrToInt(btnCursor.ElementHandle.getAttribute('position'));

  // Position of the cursor
  if CursorPosition <> -1 then

    // New Gong
    if PositionsG[CursorPosition] = -1 then
      GongID := Length(Gongs);
      SetLength(Gongs, GongID + 1);
      SetLength(GongsP, GongID + 1);
      PositionsG[CursorPosition] := GongID;
      GongsP[GongID] := CursorPosition;

      Gongs[GongID] := TWebHTMLDiv.Create('Gong-'+IntToStr(GongID));
      Gongs[GongID].Parent := divButtons;



      Gongs[GongID]'width',FloatToStrF(HexRadius * 2,ffGeneral,5,3)+'px');
      Gongs[GongID]'height',FloatToStrF(HexRadius * 2,ffGeneral,5,3)+'px');

      Gongs[GongID].HTML.Text := '<div class="GongContent" style="color:white;">'+IntToStr(GongID+1)+'</div>';

      (document.getElementById('BG-'+IntToStr(CursorPosition)) as TJSHTMLElement).style.setProperty('animation-name','jiggle');

With this in place, we can, while in Change Mode, click on any "valid" background hexagon (IE, not one of the hexagons containing a primary button) and then click the pencil to add a new HexaGong to the mix. If it isn't already populated. Ultimately, this will bring up an Options interface which we'll touch upon a little later in this post and then explore more fully in the next post. For now, we've got our HexaGong placeholders. Let's look at moving them around.

Drag and Swap.

In our Moving Elements post, we looked at a variety of different options when dragging and dropping elements on the page, particularly when there are fixed positions and those positions are already populated. The existing element could be left as-is and the new element added on top. Or the existing element could be removed entirely.  Or it could be moved out of the way. Or it could swap positions with the incoming element.  This is the option we're going to use here, coined "drag and swap". 

Dragging a HexaGong to an empty spot will simply move it there. Dragging it to an occupied spot will send the existing HexaGong to wherever the dragged HexaGong came from. And we'd like this to be animated a little - both moving the new HexaGong into place, as well as moving the existing HexaGong into the original place.

We covered much of this in the Moving Elements post, but here we have a few more wrinkles. First, there are only certain positions we can drag a HexaGong to - the PositionT (targets) array will help us with this. Second, we've got a pesky cursor that we could be moving, or if we're not moving the cursor, we'd like the cursor to end up with whatever we've moved.

But the biggest problem we have is that the elements we're moving are children of divBackground. So whenever we move one of them (the current HexaGong, a HexaGong potentially occupying a destination, and the btnCursor element), they first have to be removed from the starting "cup" and placed at a level higher, moved, and then dropped into the destination "cup". This could potentially happen three times for one drag-and-drop operation.  And this is in addition to the work we were already doing in finding the target position based on where we dropped the Hexagon, to begin with.  

Fortunately, InteractJS has all the mechanisms we need to make this work. This is implemented in JavaScript. As in the Moving Elements post, this code sets up the "dragswap" class, so any element that has this class can be dragged and swapped with this mechanism. This is applied to all of the HexaGong elements and the cursor, all configured as children of the background hexagon <div> elements. There are three top-level code blocks to note.

  • on click event: This deals with moving the cursor to the HexaGong when it is clicked. This is similar to what happens when an empty background element is clicked on, but when clicking on an actual HexaGong, that event will (deliberately) not catch this event, so we handle it separately here.
  • on start drag: Extract the HexaGong (or the cursor) from the "cup" that it is in and position it above the same location in preparation for dragging it around.
  • on end drag: Find out where the nearest target Position is. Then figure out what has to move. Either just the cursor, just one HexaGong (and the cursor), or two HexaGongs (and the cursor). Then set them in motion, potentially having to move a HexaGong in the target Position out of its cup, over to the original position, and back into that cup instead.

Not for the squeamish, but here's what we've got.

  // Configure InteractJS for Drag & Swap functionality
    var This = pas.Unit1.Form1;

      .on('click', event => {
          if (This.PositionsG[btnCursor.parentElement.getAttribute('position')] == -1) {
        }, { capture: true })

        inertia: true,
        modifiers: [],

        onstart: function(event) {
          // When dragging begins, remove element from its hexagon and add it to the background at the same spot
          if ('CursorButton')) {
          } else {

          // Ensure that it is above everything else, and stop whatever jiggling might be going on

        onend: async function(event) {
          // When dragging ends, apply the InteractJS movement data to the element
          var PosX = parseFloat((parseFloat('px','')) + parseFloat('data-x'))));
          var PosY = parseFloat((parseFloat('px','')) + parseFloat('data-y'))));
'top', PosY + 'px');
'left', PosX + 'px');

'top', '-1000px');
'left', '-1000px');

          // Find nearest Position
          var minDistance = 999999;
          var NewX = 0
          var NewY = 0;
          var dist = 0;
          var position = -1;
          for (var i = 0; i < This.PositionsX.length; i++) {
            dist = Math.sqrt(Math.pow(This.PositionsX[i] - PosX,2) + Math.pow(This.PositionsY[i] - PosY,2));
            if ((dist < minDistance) && (This.PositionsT[i] == true)) {
              minDistance = dist;
              NewX = This.PositionsX[i];
              NewY = This.PositionsY[i];
              position = i;

          // If we're just draging the cursor around....
          if ('CursorButton')) {

          else {

            // Figure out where it started from
            var gongid ='gongid');
            var oldposition = This.GongsP[gongid];

            // When dropping on populated Position, prepare to swap
            if ((This.PositionsG[position] !== gongid) && (This.PositionsG[position] !== -1)) {

              // Get the element that we're swapping
              var swapid = This.PositionsG[position];
              var swapel = document.getElementById('Gong-'+swapid);

              // And this is where it is ultimately headed
              var OldX = This.PositionsX[oldposition];
              var OldY = This.PositionsY[oldposition];

              // Move it from its hexagon container into the same background as the element we're working wtih
    'top', NewY + 'px');
    'left', NewX + 'px');
    'transition','top 0.2s linear, left 0.2s linear');

              // Update data about this element we're swapping
              This.GongsP[swapid] = oldposition;
              This.PositionsG[oldposition] = swapid;

            // When dropping on empty positionm, just update the old position information
            else if (This.PositionsG[position] == -1) {
              This.PositionsG[oldposition] = -1

            if (This.PositionsG[btnCursor.getAttribute('position')] == -1) {

            // Update Position Information
            This.PositionsG[position] = gongid;
            This.GongsP[gongid] = position;

            // This gives the page a chance to update everything we just changed
            await sleep(0);

            // Move our element to its new position
  'transition','top 0.2s linear, left 0.2s linear');
  'top', NewY + 'px');
  'left', NewX + 'px');

            // After it has been moved, drop it back into its hexagon holder
            // Also, move the cursor to this position as well

    'top', '0px');


            // If we were swapping elements, move the swapped element to the prior location
            // and then drop it into its hexagon holder too
            if (swapel !== undefined) {
     = OldY + 'px';
     = OldX + 'px';
        listeners: {
          move: dragMoveListener

    function dragMoveListener (event) {
      var target =
      var x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx
      var y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy = 'translate(' + x + 'px, ' + y + 'px)'
      target.setAttribute('data-x', x)
      target.setAttribute('data-y', y)
    window.dragMoveListener = dragMoveListener

There's a hack-ish little await() thrown in to help ensure that the browser gets updated with some of the intermediate steps. And plenty of extra jiggling handled by the addition or removal of the "animation-name" property whenever we want to start/stop the jiggling. The idea is that everything we can interact with will jiggle, but not while we're directly interacting with it (dragging it or moving it).  

Jiggle Mode.

While we're on the topic of jiggling, we can update our Change Mode function to start all of the existing HexaGongs jiggling by using the following. We also assign the "dragswap" class at the same time, as we don't want to be able to drag anything around unless we are in Change Mode. When Change Mode stops, the reverse removes the "dragswap" class and any "animation-name" properties so that there's no residual jiggling going on. This is in addition to dealing with the presentation of the primary and secondary buttons, which we addressed in the last post.

procedure TForm1.btnChangeClick(Sender: TObject);
  i: Integer;

  if (MillisecondsBetween(Now, LastClick) > 1000) or (Sender = nil) then
    LastClick := Now;

    if (Sender = btnChange) and (ChangeMode = False) then
      ChangeMode := True;

      i := 0;
      while i < Length(Gongs) do
        (Gongs[i].ElementHandle.parentElement as TJSHTMLElement).style.setProperty('animation-name','jiggle');
        i := i + 1;


        setTimeout(function() {'opacity','0.2'); }, 400 );
        setTimeout(function() {'opacity','0.2'); }, 200 );
        setTimeout(function() {'opacity','0.2'); }, 0 );

      ChangeMode := False;

      i := 0;
      while i < Length(Gongs) do
        i := i + 1;

        // Just in case some stragglers are left behind?!
        var hex = document.querySelectorAll('.Hexagon');

        setTimeout(function() {'opacity','0'); }, 400 );
        setTimeout(function() {'opacity','0'); }, 200 );
        setTimeout(function() {'opacity','0'); }, 0 );
        setTimeout(function() {


With that in place, we end up with the following. Here, we're adding three HexaGongs, using the cursor to select the Position, and then dragging them (and by extension, the cursor) around to different Positions. And when dragging to an occupied Position, the two HexaGongs are swapped. All with a bit of animation to smooth things over.

TMS Software Delphi  Components
HexaGong Movement.

That takes care of everything we're after here - the ability to move things around, so we can arrange our HexaGongs in a particular way. 

Dimensionally Challenged.

But there's a bit of a wrinkle here. If we have a certain arrangement of HexaGongs on a particular hexagonal grid, what happens when we change the dimensions of the grid? Nothing good! As the Positions for the HexaGongs are just an integer, the default behavior would simply re-flow the HexaGongs within a different set of dimensions, messing up whatever pattern was in place previously.

To address this, we'll need to update our grid resizing to include an algorithm to adjust the Positions of the HexaGongs. Here's what it looks like when adding an extra pair of columns. Something similar is done when they are removed. We're basically copying the row and column Positions from one set of dimensions to another.

procedure TForm1.btnScalePlusClick(Sender: TObject);
  i: Integer;
  j: integer;
  r: integer;
  c: integer;


  if Zoomlevel < 20 then
    ZoomLevel := ZoomLevel + 1;

    // Update Gong Positions within new hexagon arrangement

    for i := 0 to Length(PositionsG) -1  do
      PositionsG[i] := -1;

    i := 0;
    while i < Length(Gongs) do
      r := StrToInt(Gongs[i].ElementHandle.getAttribute('row'));
      c := StrToInt(Gongs[i].ElementHandle.getAttribute('column'));
      j := 0;
      while ((PositionsR[j] <> r) or (PositionsC[j] <> c)) and (j < Length(PositionsR)) do
        j := j + 1;
      if j < Length(PositionsR) then
        GongsP[i] := j;
        PositionsG[GongsP[i]] := i;
        Gongs[i].ElementHandle.setAttribute('position', IntToStr(j));
        Gongs[i].ElementHandle.setAttribute('row', IntToStr(PositionsR[j]));
        Gongs[i].ElementHandle.setAttribute('column', IntToStr(PositionsC[j]));
      i := i + 1;



With this in place, we can easily add extra columns to our grid without any adverse effects. If we want to reduce the columns, that's fine too, just that if any HexaGongs are on the right edge, they may end up getting wrapped.  Not much can be done about that. Likewise, if there are fewer rows available, the HexaGongs simply won't be displayed. They will still be there, so increasing the rows again will restore them.

Note also that whenever the window is resized, the hexagonal grid is regenerated. This means that we have to take all the HexaGongs out of the "cup" hexagons that are in the background, and then add them back in afterward.  The removal is taken care of with the ConfigureButtons method we covered previously. Adding them back in involves the following. Note the last call to appendChild again.

  if StrToInt(btnCursor.ElementHandle.getAttribute('position')) <> -1
  then document.getElementById('BG-'+btnCursor.ElementHandle.getAttribute('position')).appendChild(btnCursor.ElementHandle);

  I := 0;
  while I < Length(Gongs) do
    Gongs[I]'width',FloatToStrF(HexRadius * 2,ffGeneral,5,3)+'px');
    Gongs[I]'height',FloatToStrF(HexRadius * 2,ffGeneral,5,3)+'px');
    I := I + 1;

This means that we can then resize, reorient, or otherwise mess around with the dimensions and it won't interfere with our HexaGongs unless we run out of space for them. All good!

Next Time.

Now that we've got our basic interface sorted out - moving HexaGongs around the page - our next post will focus on what goes into the HexaGongs themselves. The Options dialog. And plenty of JavaScript, HTML, and CSS to work hexagons into every nook and cranny of our interface. 

HexaGongs website.
HexaGongs repository on GitHub.
HexaGongs XData repository on GitHub.

Related Posts.

HexaGongs Part 1: Background
HexaGongs Part 2: Interface
HexaGongs Part 3: Options (Edits, Memos, Buttons, Colors, Trackbars)
HexaGongs Part 4: Options (Image and Audio)
HexaGongs Part 5: Deployment

Follow Andrew on 𝕏 at @WebCoreAndMore or join our
Web Core and More Community.

Andrew Simard


This blog post has not received any comments yet.

Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.

All Blog Posts  |  Next Post  |  Previous Post