)
}
}
)
(
}
{
)
)
(
)
(
(
{
}
)
(
)
}
)
)
{
(
(
)
)
}
)
(
}

isPointInPath Canvas

  1. const canvas = document.createElement('canvas');
  2. const c = canvas.getContext('2d');
  3. let mouseX = 0, mouseY = 0;
  4.  
  5. canvas.width = 400;
  6. canvas.height = 400;
  7. document.body.appendChild(canvas);
  8. document.body.style.margin = 0;
  9.  
  10. c.fillStyle = 'black';
  11. c.fillRect(0, 0, canvas.width, canvas.height);
  12.  
  13. document.addEventListener('mousemove', e => {
  14.   mouseX = e.clientX;
  15.   mouseY = e.clientY;
  16. });
  17.  
  18. // no scroll on mobile: 
  19. document.addEventListener('touchmove', 
  20.   e => e.preventDefault(), { passive: false });
  21.  
  22. document.addEventListener('touchmove', e => {
  23.   mouseX = e.touches[0].clientX;
  24.   mouseY = e.touches[0].clientY;
  25. });
  26.  
  27. const loop = () => {
  28.   c.fillStyle = 'black';
  29.   c.fillRect(0, 0, canvas.width, canvas.height); 	
  30.   c.lineWidth = 3;
  31.   c.strokeStyle = 'blue';
  32.   c.beginPath();
  33.   c.moveTo(20, 20);
  34.   c.lineTo(110, 20);
  35.   c.lineTo(110, 110);
  36.   c.lineTo(20, 110);
  37.   c.closePath();
  38.  
  39.   if (c.isPointInPath(mouseX, mouseY)) {
  40.     c.strokeStyle = 'white';
  41.     c.fillStyle = 'red';
  42.     c.fill();
  43.   }
  44.   c.stroke();
  45.  
  46.   requestAnimationFrame(loop);
  47. };
  48.  
  49. loop();

See if a point is with a path inside canvas. Take a look at MDN for more info.

Make a Grid

  1. const cellSize = 25;
  2. const cols = 10;
  3. const rows = 20;
  4.  
  5. function makeDot(x, y) {
  6.   const dot = document.body.appendChild(
  7.     document.createElement('div')
  8.   );
  9.  
  10.   dot.classList.add('cell');
  11.  
  12.   Object.assign(dot.style, {
  13.     position: 'absolute',
  14.     left: `${x}px`,
  15.     top: `${y}px`,
  16.     width: `${cellSize}px`,
  17.     height: `${cellSize}px`,
  18.     outline: '1px solid black',
  19.     cursor: 'pointer',
  20.     background: 'gray'
  21.   });
  22.  
  23.   return dot;
  24. }
  25.  
  26. for (let y = 0; y < rows; y++) {
  27.   for (let x = 0; x < cols; x++) {
  28.     makeDot(x * cellSize, y * cellSize);
  29.   }
  30. }
  31.  
  32. // make a cell red when it is rolled over
  33. document.addEventListener('mouseover', e => {
  34.   if (e.target.classList.contains('cell')) {
  35.     e.target.style.background = 'red';
  36.   }
  37. });

Here is a simple example for arranging divs in a grid.

// css // dom // javascript // math // tricks // ui

Proxy Quick DOM

  1. const spec = {
  2.   get(o, key) {
  3.     return o[key] != null ? 
  4.       o[key] : o[key] = (...args) => {
  5.         const el = document.createElement(key);
  6.         args.forEach(arg => { 
  7.           if (typeof arg === 'string') {
  8.             const span = document.createElement('span');
  9.             span.innerHTML = arg;
  10.             el.appendChild(span);
  11.           } else if (typeof arg === 'object') {
  12.             if (arg.tagName != null) {
  13.               el.appendChild(arg);
  14.             } else {
  15.               for (let i in arg) {
  16.                 el.setAttribute(i, arg[i]);
  17.               }
  18.             }
  19.           }
  20.         });
  21.         return el;
  22.       }
  23.   },
  24.   set(o, key, v) {
  25.     o[key] = v;
  26.   }
  27. }
  28.  
  29. const dom = new Proxy({}, spec);
  30.  
  31. document.body.appendChild(
  32.   dom.div(
  33.     dom.button('cool'), 
  34.     dom.h2('some text', { style: 'font-style: italic' }), 
  35.     dom.br(), 
  36.     dom.input({ placeholder: 'zevan' })
  37.   )
  38. );
  39.  
  40. const { div, input, label } = dom;
  41. document.body.appendChild(
  42.   div(
  43.     label(
  44.       'Slider:',
  45.       { 
  46.         for: 'slider', 
  47.         style: 'padding:1em;display:block' 
  48.       },
  49.       input({ id: 'slider', type: 'range' })
  50.     )
  51.   )
  52. );

In this snippet a proxy is used that makes all html node tagNames valid methods. Each method can take strings and HTMLElements as arguments in any order to create a dom structure. This may look familiar to people who have looked a bit deeper into the inner workings of some of the popular UI libraries of the last decade.

// dom // hacks // javascript // proxies // tricks // ui

ClassName Behaviors

  1. const ui = document.body.appendChild(
  2.   document.createElement('div')
  3. );
  4.  
  5. ui.innerHTML = `
  6.   <button class="blue circle redBorder moveDown">click</button>
  7.   <button class="red circle sayHi">click</button>  
  8.   <button class="green circle noBorder moveDown sayHi randomBgColor">click</button>
  9.  
  10.   <style>
  11.     * { -webkit-user-select: none; user-select: none; }
  12.     body, html {
  13.       width: 100%; height: 100%;
  14.     }
  15.     button { 
  16.       position: relative;
  17.       top: 0;
  18.       margin: .5em; 
  19.       cursor: pointer; 
  20.       color: white;
  21.       transition: all 200ms ease-out;
  22.       text-shadow: 1px 2px 1px black;
  23.     }
  24.     .circle {
  25.       width: 50px;
  26.       height: 50px;
  27.       border-radius: 500px;
  28.     }
  29.     .blue {
  30.       background: #275ba1;
  31.     }
  32.     .red {
  33.       background: red;
  34.     }
  35.     .green {
  36.       background: green;
  37.     }
  38.     .redBorder {
  39.       border: 2px solid red;
  40.     }
  41.     .noBorder {
  42.       border: none;
  43.     }
  44.   </style>
  45. `;
  46.  
  47. const actions = {
  48.   moveDown(e) {
  49.     e.target.style.top = `${parseFloat(e.target.style.top || 0) + 30}px`;
  50.   },
  51.   sayHi(e) {
  52.     e.target.innerHTML = [
  53.       'hi', 'hello', 'hey', 'aloha', 'what\'s up'
  54.     ][
  55.       Math.floor(Math.random() * 5)
  56.     ];
  57.     e.target.style.transform = `
  58.       rotate(${Math.random() * 40 - 20}deg) 
  59.       scale(${Math.random() * .3 + 1})`
  60.   },
  61.   randomBgColor(e) {
  62.     const col = `hsl(${Math.random() * 360}deg, 50%, 50%)`
  63.     e.target.style.background = col;
  64.   }
  65. };
  66.  
  67. document.body.addEventListener('click', e => {
  68.   // combine as many actions as we want
  69.   [...e.target.classList].forEach(cls => { 
  70.     const action = actions[cls];
  71.     if (action != null) action(e);
  72.   });
  73. });

This snippet takes the ideas from yesterdays post and goes one level further. This associates behavior with class names, so the class names can be combined to mix and match behavior.

In this case, combining classes like this green circle noBorder moveDown sayHi randomBgColor will cause the element in question to “move down”, “say hi” and randomize its background color when it is clicked. Click the “Try it out” to get a better idea.

// animation // css // dom // events // javascript // tricks // ui

Easy Mouse Events

  1. const ui = document.body.appendChild(
  2.   document.createElement('div')
  3. );
  4.  
  5. ui.innerHTML = `
  6.   <button class="hello">say hello</button>
  7.   <button class="make-box">make a box</button>
  8.   <button class="remove-boxes">remove boxes</button>
  9.   <button class="consoleLog red">log in console</button>
  10.  
  11.   <style>
  12.     button { margin: .5em; cursor: pointer; }
  13.     .red { background: red; color: white; }
  14.     .box {
  15.       position: relative;
  16.       float: left;
  17.       width: 30px;
  18.       height: 30px;
  19.       margin: 1em;
  20.       background: blue;
  21.     }
  22.   </style>
  23. `;
  24.  
  25. const actions = {
  26.   hello(e) {
  27.     alert(e.target.className);
  28.   },
  29.  
  30.   ['make-box']() {
  31.     const box = document.body.appendChild(
  32.       document.createElement('div')
  33.     );
  34.     box.classList.add('box');
  35.   },
  36.  
  37.   ['remove-boxes']() {
  38.     const boxes = [...document.querySelectorAll('.box')];
  39.     const num = boxes.length;
  40.     boxes.forEach((el) => el.parentNode.removeChild(el));
  41.  
  42.    alert(
  43.       num === 0
  44.         ? 'no boxes to remove'
  45.         : `removing ${num} box${num > 1 ? 'es' : ''}`
  46.     );
  47.   },
  48.  
  49.   consoleLog(e) {
  50.     console.log('camelCase instead of kebab-case :D');
  51.   },
  52. };
  53.  
  54. document.addEventListener('mousedown', (e) => {
  55.   const action = actions[e.target.classList[0]];
  56.   if (action != null) action(e);
  57. });

This is a powerful little pattern for managing mouse/touch events. Rather than assigning many listeners, this snippet has one listener on the document. Anytime the page is clicked, we look at the event targets classList and use its first value as a key in an actions object.

I have used this or some variation of it many many times over the years. With a little customization it scales well into large projects. I always find myself on the fence about class naming conventions… kebab-case vs camelCase – probably because it just doesn’t matter that much. For large projects, each main section of the UI will have its own document.addEventListener just for organizational purposes.

Variations

The choice to use the first class as the key for the action is pretty arbitrary. Depending on how you like to set things up you could use the last value of the classList, the element id, the element name, or a custom data attribute etc… Like this:

  1. // id
  2. const action = actions[e.target.id];
  3.  
  4. // name
  5. const action = actions[e.target.name];
  6.  
  7. // data attribute <button data-my-id="test">test</button>
  8. const action = actions[e.target.dataset.myId];
// dom // events // javascript // tricks // ui
snippet.zone ~ 2021-24 /// {s/z}