I have a heavily optimized JavaScript app, a highly interactive graph editor. I now started profiling it (using Chrome dev-tools) with massive amounts of data (thousands of shapes in the graph), and I'm encountering a previously unusual performance bottleneck, Hit Test.
| Self Time | Total Time | Activity |
|-----------------|-----------------|---------------------|
| 3579 ms (67.5%) | 3579 ms (67.5%) | Rendering |
| 3455 ms (65.2%) | 3455 ms (65.2%) | Hit Test | <- this one
| 78 ms (1.5%) | 78 ms (1.5%) | Update Layer Tree |
| 40 ms (0.8%) | 40 ms (0.8%) | Recalculate Style |
| 1343 ms (25.3%) | 1343 ms (25.3%) | Scripting |
| 378 ms (7.1%) | 378 ms (7.1%) | Painting |
This takes up 65% of everything (!), remaining a monster bottleneck in my codebase. I know this is the process of tracing the object under the pointer, and I have my useless ideas about how this could be optimized (use fewer elements, use fewer mouse events, etc.).
Context: The above performance profile shows a "screen panning" feature in my app, where the contents of the screen can be moved around by dragging the empty area. This results in lots of objects being moved around, optimized by moving their container instead of each object individually. I made a demo.
Before jumping into this, I wanted to search for the general principles of optimizing hit testing (those good ol' "No sh*t, Sherlock" blog articles), as well as if any tricks exist to improve performance on this end (such as using translate3d to enable GPU processing).
I tried queries like js optimize hit test, but the results are full of graphics programming articles and manual implementation examples -- it's as if the JS community hadn't even heard of this thing before! Even the chrome devtools guide lacks this area.
Edit: there is this question, but it doesn't help much: What is the Chrome Dev Tools "Hit Test" timeline entry?
So here I am, proudly done with my research, asking: how do I get about optimizing native hit testing in JavaScript?
I prepared a demo that demonstrates the performance bottleneck, although it's not exactly the same as my actual app, and numbers will obviously vary by device as well. To see the bottleneck:
Go to the Timeline tab on Chrome (or the equivalent of your browser)
Start recording, then pan around in the demo like a mad-man
Stop recording and check the results
A recap of all significant optimizations I have already done in this area:
moving a single container on the screen instead of moving thousands of elements individually
using transform: translate3d to move container
v-syncing mouse movement to screen refresh rate
removing all possible unnecessary "wrapper" and "fixer" elements
using pointer-events: none on shapes -- no effect
Additional notes:
the bottleneck exists both with and without GPU acceleration
testing was only done in Chrome, latest
the DOM is rendered using ReactJS, but the same issue is observable without it, as seen in the linked demo
Interesting, that pointer-events: none has no effect. But if you think about it, it makes sense, since elements with that flag set still obscure other elements' pointer events, so the hittest has to take place anyways.
What you can do is put a overlay over critical content and respond to mouse-events on that overlay, let your code decide what to do with it.
This works because once the hittest algorithm has found a hit, and I'm assuming it does that downwards the z-index, it stops.
With overlay
// ================================================
// Increase or decrease this value for testing:
var NUMBER_OF_OBJECTS = 40000;
// Wether to use the overlay or the container directly
var USE_OVERLAY = true;
// ================================================
var overlay = document.getElementById("overlay");
var container = document.getElementById("container");
var contents = document.getElementById("contents");
for (var i = 0; i < NUMBER_OF_OBJECTS; i++) {
var node = document.createElement("div");
node.innerHtml = i;
node.className = "node";
node.style.top = Math.abs(Math.random() * 2000) + "px";
node.style.left = Math.abs(Math.random() * 2000) + "px";
contents.appendChild(node);
}
var posX = 100;
var posY = 100;
var previousX = null;
var previousY = null;
var mousedownHandler = function (e) {
window.onmousemove = globalMousemoveHandler;
window.onmouseup = globalMouseupHandler;
previousX = e.clientX;
previousY = e.clientY;
}
var globalMousemoveHandler = function (e) {
posX += e.clientX - previousX;
posY += e.clientY - previousY;
previousX = e.clientX;
previousY = e.clientY;
contents.style.transform = "translate3d(" + posX + "px, " + posY + "px, 0)";
}
var globalMouseupHandler = function (e) {
window.onmousemove = null;
window.onmouseup = null;
previousX = null;
previousY = null;
}
if(USE_OVERLAY){
overlay.onmousedown = mousedownHandler;
}else{
overlay.style.display = 'none';
container.onmousedown = mousedownHandler;
}
contents.style.transform = "translate3d(" + posX + "px, " + posY + "px, 0)";
#overlay{
position: absolute;
top: 0;
left: 0;
height: 400px;
width: 800px;
opacity: 0;
z-index: 100;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
user-select: none;
}
#container {
height: 400px;
width: 800px;
background-color: #ccc;
overflow: hidden;
}
#container:active {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
.node {
position: absolute;
height: 20px;
width: 20px;
background-color: red;
border-radius: 10px;
pointer-events: none;
}
<div id="overlay"></div>
<div id="container">
<div id="contents"></div>
</div>
Without overlay
// ================================================
// Increase or decrease this value for testing:
var NUMBER_OF_OBJECTS = 40000;
// Wether to use the overlay or the container directly
var USE_OVERLAY = false;
// ================================================
var overlay = document.getElementById("overlay");
var container = document.getElementById("container");
var contents = document.getElementById("contents");
for (var i = 0; i < NUMBER_OF_OBJECTS; i++) {
var node = document.createElement("div");
node.innerHtml = i;
node.className = "node";
node.style.top = Math.abs(Math.random() * 2000) + "px";
node.style.left = Math.abs(Math.random() * 2000) + "px";
contents.appendChild(node);
}
var posX = 100;
var posY = 100;
var previousX = null;
var previousY = null;
var mousedownHandler = function (e) {
window.onmousemove = globalMousemoveHandler;
window.onmouseup = globalMouseupHandler;
previousX = e.clientX;
previousY = e.clientY;
}
var globalMousemoveHandler = function (e) {
posX += e.clientX - previousX;
posY += e.clientY - previousY;
previousX = e.clientX;
previousY = e.clientY;
contents.style.transform = "translate3d(" + posX + "px, " + posY + "px, 0)";
}
var globalMouseupHandler = function (e) {
window.onmousemove = null;
window.onmouseup = null;
previousX = null;
previousY = null;
}
if(USE_OVERLAY){
overlay.onmousedown = mousedownHandler;
}else{
overlay.style.display = 'none';
container.onmousedown = mousedownHandler;
}
contents.style.transform = "translate3d(" + posX + "px, " + posY + "px, 0)";
#overlay{
position: absolute;
top: 0;
left: 0;
height: 400px;
width: 800px;
opacity: 0;
z-index: 100;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
user-select: none;
}
#container {
height: 400px;
width: 800px;
background-color: #ccc;
overflow: hidden;
}
#container:active {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
.node {
position: absolute;
height: 20px;
width: 20px;
background-color: red;
border-radius: 10px;
pointer-events: none;
}
<div id="overlay"></div>
<div id="container">
<div id="contents"></div>
</div>
One of the problems is that you're moving EVERY single element inside your container, it doesn't matter if you have GPU-acceleration or not, the bottle neck is recalculating their new position, that is processor field.
My suggestion here is to segment the containers, therefore you can move various panes individually, reducing the load, this is called a broad-phase calculation, that is, only move what needs to be moved. If you got something out of the screen, why should you move it?
Start by making instead of one, 16 containers, you'll have to do some math here to find out which of these panes are being shown. Then, when a mouse event happens, move only those panes and leave the ones not shown where they are. This should reduce greatly the time used to move them.
+------+------+------+------+
| SS|SS | | |
| SS|SS | | |
+------+------+------+------+
| | | | |
| | | | |
+------+------+------+------+
| | | | |
| | | | |
+------+------+------+------+
| | | | |
| | | | |
+------+------+------+------+
On this example, we have 16 panes, of which, 2 are being shown (marked by S for Screen). When a user pans, check the bounding box of the "screen", find out which panes pertain to the "screen", move only those panes. This is theoretically infinitely scalable.
Unfortunately I lack the time to write the code showing the thought, but I hope this helps you.
Cheers!
There's now a CSS property in Chrome, content-visibility: auto, that helps to prevent hit-testing when DOM elements are out of view. See web.dev.
The content-visibility property accepts several values, but auto is the one that provides immediate performance improvements. An element that has content-visibility: auto gains layout, style and paint containment. If the element is off-screen (and not otherwise relevant to the user—relevant elements would be the ones that have focus or selection in their subtree), it also gains size containment (and it stops painting and hit-testing its contents).
I couldn't replicate the issues of this demo, likely due to pointer-events: none now working as intended, as #rodrigo-cabral mentioned, however I was having significant issues while dragging using HTML5 drag and drop due to having a large number of elements with dragOver or dragEnter event handlers, most of which were on off screen elements (virtualising these elements came with significant drawbacks, so we haven't done so yet).
Adding the content-visibility: auto property to the elements that had the drag event handlers significantly improved hit-test times (from 12ms down to <2ms).
This does come with some caveats, such as causing elements to render as though they have overflow: hidden, or requiring contain-intrinsic-size to be set on the elements to ensure they take up that space when they're offscreen, but it's the only property I've found that helps reduce hit-test times.
NOTE: Attempting to use contain: layout style paint size alone did not have any impact on reducing hit-test times.
Related
I have a large div and smaller siblings divs positioned inside it like this:
.large{
height:20rem;
width:20rem;
background-color:red;
position:absolute;
}
.item1{
height:5rem;
width:5rem;
background-color:blue;
top:1rem;
position:absolute;
}
.item2{
height:5rem;
width:5rem;
background-color:green;
top:3rem;
left:2rem;
position:absolute;
}
.item3{
height:5rem;
width:5rem;
background-color:yellow;
top:1rem;
left:6rem;
position:absolute;
}
<div class="large"></div>
<div class="item1"></div>
<div class="item2"></div>
<div class="item3"></div>
How do I get all the small divs within the large div dimensions?
Is there something similar to elementsFromPoint? Maybe something like elementsFromArea
Edit:
assume .large spans 320 pixels x 320 pixels
and I have multiple smaller divs on my screen, which can either be overlapping .large or outside it
How do I find divs which are overlapping .large?
Maybe we could get the position of .large & we already have the height and width of it and add it to some function like this:
elementsFromArea(large_x,large_y,large_height,large_width);
This should return an array of all the divs within that given range
(.large is merely for reference sake, I simply want to pass any given square area & find all the divs lying within it )
Bounty Edit:
The solution provided by #A Haworth works but I'm looking for a solution which doesn't involve having to loop and check every single element
this fiddle explains what I'm ultimately trying to achieve
Any clever work around will be accepted too!
You can use getBoundingClientRect to find the left, right, top and bottom bounds of each element.
Then test whether there is overlap with the large element by seeing whether the left is to the left of the right side of the large element and so on:
if ( ((l <= Right) && (r >= Left)) && ( (t <= Bottom) && (b >= Top)) )
To give a more thorough test, in this snippet the blue element has been pushed down so it only partially overlaps the large one and the yellow element doesn't overlap at all.
const large = document.querySelector('.large');
const largeRect = large.getBoundingClientRect();
const Left = largeRect.left;
const Right = largeRect.right;
const Top = largeRect.top;
const Bottom = largeRect.bottom;
const items = document.querySelectorAll('.large ~ *');
let overlappers = [];
items.forEach(item => {
const itemRect = item.getBoundingClientRect();
const l = itemRect.left;
const r = itemRect.right;
const t = itemRect.top;
const b = itemRect.bottom;
if (((l <= Right) && (r >= Left)) && ((t <= Bottom) && (b >= Top))) {
overlappers.push(item);
}
});
console.log('The items with these background colors overlap the large element:');
overlappers.forEach(item => {
console.log(window.getComputedStyle(item).backgroundColor);
});
.large {
height: 20rem;
width: 20rem;
background-color: red;
position: absolute;
}
.item1 {
height: 5rem;
width: 5rem;
background-color: blue;
top: 19rem;
position: absolute;
}
.item2 {
height: 5rem;
width: 5rem;
background-color: green;
top: 3rem;
left: 2rem;
position: absolute;
}
.item3 {
height: 5rem;
width: 5rem;
background-color: yellow;
top: 1rem;
left: 26rem;
position: absolute;
}
<div>
<div class="large"></div>
<div class="item1"></div>
<div class="item2"></div>
<div class="item3"></div>
</div>
Note, this snippet tests only those elements which are siblings of large in the CSS sense, that is that follow large. If you want all siblings whether they follow large or come before it then go back up to large's parent and get all its children (which will of course include large).
The IntersectionObserver API describes exactly what you are looking for. It's a relatively new API so I'm not surprised the other answers have not referenced it.
I have personally used it in a lazy loading context for displaying large tables without rendering 9001 rows at once. In my case, I would use the IntersectionObserver to determine when the last table row was in the user's field of view, and then I would load additional rows. It's very performant as it doesn't require any loops that poll the position of DOM elements, and the browser is free to optimize it however it likes.
Stealing from MDN, here's a simple way to create an IntersectionObserver. I've commented out options which I don't think you need.
let options = {
root: document.querySelector('.large'),
// rootMargin: '0px',
// threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
The callback is a function that fires whenever an element's intersection of .large changes by a certain threshold. If threshold = 0 (the default value and what I think you want in your case), then it will fire even if only 1 pixel overlaps.
Once you've created an IntersectionObserver with .large as the root, you will then want to .observe() the smaller divs so the IntersectionObserver can report on when they intersect .large.
Again, stealing from MDN, the format of the callback is as follows. Please note that the callback fires on intersection changes, meaning that if a smaller div that used to intersect .large no longer does, it will be in the list of entries. To get elements that are intersecting .large you will want to filter entries such that only those where entry.isInterecting === true are present. From the filtered list of entries you can then grab entry.target from every entry.
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
The solution provided by #A Haworth works but I'm looking for a solution which doesn't involve having to loop and check every single element
I don't know how to achieve this without a loop, if we are handle an array of elements, but you can test this solution with the resizeObserver and loops.
// Init elements
const items = [...document.querySelectorAll('.item')];
const frame = document.getElementById('frame');
const resultElement = document.getElementById('for-result');
// Creating an array of properties
// Math.trunc() removing any fractional digits
const itemsProperties = items.map(item => {
return {
width: item.getBoundingClientRect().width,
height: item.getBoundingClientRect().height,
x: Math.trunc(item.getBoundingClientRect().x),
y: Math.trunc(item.getBoundingClientRect().y),
};
});
function within_frame(frameSize) {
const inside = [];
for (const i in itemsProperties) {
// Determine current height and width of the square
// Because X, Y is TOP, LEFT, and we need RIGHT, BOTTOM values.
const positionY = itemsProperties[i].height + itemsProperties[i].y;
const positionX = itemsProperties[i].width + itemsProperties[i].x;
// If the position square less than or equal to the size of the inner frame,
// then we will add values to the array.
if (
positionY <= frameSize.blockSize &&
positionX <= frameSize.inlineSize
) {
inside.push(itemsProperties[i]);
}
}
//returns all the elements within the frame bounds
return inside;
}
// Initialize observer
const resizeObserver = new ResizeObserver(entries => {
// Determine height and width of the 'frame'
const frameSize = entries[0].borderBoxSize[0];
// Return an array of values inside 'frame'
const result = within_frame(frameSize);
//console.log(result);
// for result
resultElement.innerHTML = result.map(
(el, idx) => `<code>square${idx + 1} position: ${el.x}px x ${el.y}px</code>`
);
});
// Call an observer to watch the frame
resizeObserver.observe(frame);
#frame {
height: 10rem;
width: 10rem;
display: inline-block;
resize: both;
border: solid black 0.5rem;
overflow: auto;
position: absolute;
z-index: 1;
}
.item {
height: 2rem;
width: 2rem;
position: absolute;
}
/* for result */
pre {
position: fixed;
right: 0.5rem;
top: 0.5rem;
border: 2px solid black;
padding: 0.5rem 1rem;
display: flex;
flex-flow: column;
}
#for-result {
font-weight: bold;
font-size: 1.5em;
}
<div id="frame"></div>
<div class="item" style="background-color: red"></div>
<div class="item" style="background-color: green; top: 50%"></div>
<div class="item" style="background-color: blue; top: 20%; left: 30%"></div>
<div class="item" style="background-color: pink; top: 60%; left: 20%"></div>
<div class="item" style="background-color: yellow; top: 25%; left: 10%"></div>
<pre id="for-result"></pre>
Heads up: A frivolous and probably useless answer
However the question itself seems quite frivolous too. No real world use case has been provided yet and I can't think of any either. Similarly, in theory my answer could be useful, but you're more likely struck by an asteroid than finding yourself needing it.
The point of posting is more that it provides some perspective on the performance of the other proposed solution. You can see you need at least hundreds of elements before performance starts being a concern.
My "answer" only works if:
items are rectangles
items cannot overlap
The potential "performance problem"
Perhaps the "not a loop" requirement refers to having a solution that doesn't require you to loop through a potentially large amount of other items in JS? This could be a valid concern, if the number of items can ever get really large.
Say that the area you're testing is relatively small compared to the items, and there are thousands of items that may or may not be inside, looping all of them might be relatively costly. Especially if you need to give each an event listener.
As already pointed out, it would be nice if a native API similar to document.getElementFromPoint existed, as that would undoubtedly be more performant than implementing in JS.
However that API does not exist. Probably because nobody ever found themselves needing it in a real world use case.
Sampling points of the frame
Now you could just use the document.ElementFromPoint API on every single point of the frame. However that would scale even worse with the frame's size.
But do we need to check every point to guarantee we're detecting all elements? Not if the elements can't overlap: since the smallest element is likely still many pixels high and wide, we could create a grid of points with those minimum values. As long as the elements don't have changing dimensions (or they can only grow) we only need to loop them once (to determine the smallest), not on updates. Note I do loop them every time, to account for setting changes. If you're sure elements have fixed dimensions you only need it once at the start of your script.
Of course, you do now have to loop over points instead. However...
In the best case scenario, where the minimum element is equally wide and high (or bigger), you only need to check 4 points. In fact I used this in a function to generate random cubes, to avoid overlap with earlier cubes.
It doesn't work on overlapping elements as document.ElementFromPoint only knows about the topmost. You could work around that by temporarily setting a z-index, but I had to stop somewhere.
Does it perform better?
I'm not sure at all whether this would ever make sense to do, but I don't immediately see another way to handle large amounts of items.
In the best case of needing just 4 points (small area to check overlap), it's hard to imagine another approach being faster, if the other approach needs to go through thousands of elements in JS. Even with up to a few tens of points it'll probably still be "fast" regardless of how many elements on the page.
let allItems = [...document.querySelectorAll('.item')];
const frame = document.getElementById('frame')
function measureLoop() {
const start = performance.now();
const large = document.querySelector('#frame');
const largeRect = large.getBoundingClientRect();
const Left = largeRect.left;
const Right = largeRect.right;
const Top = largeRect.top;
const Bottom = largeRect.bottom;
const items = document.querySelectorAll('#frame ~ *');
let overlappers = [];
items.forEach(item => {
const itemRect = item.getBoundingClientRect();
const l = itemRect.left;
const r = itemRect.right;
const t = itemRect.top;
const b = itemRect.bottom;
if (((l <= Right) && (r >= Left)) && ((t <= Bottom) && (b >= Top))) {
overlappers.push(item);
}
});
document.getElementById('result-loop').innerHTML = overlappers.length;
document.getElementById('time-loop').innerHTML = performance.now() - start;
}
function randomColor() {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
function within_frame(frame, items) {
const rect = frame.getBoundingClientRect();
const frameX = rect.left;
const frameY = rect.top;
const frameWidth = frame.clientWidth;
const frameHeight = frame.clientHeight;
const smallestWidth = Math.min(...(items.map(i => i.clientWidth)));
const smallestHeight = Math.min(...(items.map(i => i.clientHeight)));
const set = new Set();
let points = 0;
const lastY = frameHeight + smallestHeight;
const lastX = frameWidth + smallestWidth;
for (let y = 0; y < lastY; y += smallestHeight) {
for (let x = 0; x < lastX; x += smallestWidth) {
points++;
const checkX = Math.min(frameX + x, rect.right)
const checkY = Math.min(frameY + y, rect.bottom)
// Note there is always a result, but sometimes it's not the elements we're looking for.
// Set takes care of only storing unique, so we can loop a small amount of elements at the end and filter.
set.add(document.elementFromPoint(checkX, checkY));
}
}
set.forEach(el => (el === frame || el === document.documentElement || !items.includes(el)) && set.delete(el))
document.getElementById('points').innerHTML = points;
return set;
}
function measure() {
// Frame needs to be on top for resizing, put it below while calculating.
frame.style.zIndex = 1;
const start = performance.now();
const result = within_frame(frame, allItems)
const duration = performance.now() - start
document.getElementById('result').innerHTML = [...result.entries()].length;
document.getElementById('time').innerHTML = duration;
// Restore.
frame.style.zIndex = 3;
}
document.getElementById('measure').addEventListener('click', () => {measure(); measureLoop();})
const overlapsExisting = (el) => {
return within_frame(el, allItems);
}
let failedGenerated = 0;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function spawnCubes() {
frame.style.zIndex = 1;
allItems.forEach(item => item.parentNode.removeChild(item));
const nPoints = document.getElementById('nCubes').value;
const cubeSize = document.getElementById('size').value;
let newItems = [];
let failedGenerated = 0;
for (let i = 0; i < nPoints && failedGenerated < 1000; i++) {
// Sleep so that stuff is drawn.
if ((i + failedGenerated) % 100 === 0) {
document.getElementById('nCubes').value = newItems.length;
await sleep(0);
}
const el = document.createElement('div');
el.className = 'item';
//el.innerHTML = i;
el.style.backgroundColor = randomColor();
el.style.top = `${Math.round(Math.random() * 90)}%`;
el.style.left = `${Math.round(Math.random() * 60)}%`;
el.style.width = `${cubeSize}px`;
el.style.height = `${cubeSize}px`;
frame.after(el);
const existingOverlapping = within_frame(el, newItems);
if (existingOverlapping.size > 0) {
i--;
failedGenerated++;
el.parentNode.removeChild(el);
continue;
}
newItems.push(el);
}
console.log('failedAttempts', failedGenerated);
allItems = newItems;
frame.style.zIndex = 3;
document.getElementById('nCubes').value = newItems.length;
}
frame.addEventListener('mouseup', () => {measure(); measureLoop()});
spawnCubes().then(() => {measure(); measureLoop();});
document.getElementById('randomize').addEventListener('click', e => {
spawnCubes().then(measure);
})
#frame {
height: 3rem;
width: 3rem;
display: inline-block;
resize: both;
border: solid black 0.1rem;
overflow: auto;
position: absolute;
z-index: 3;
}
.item {
height: 1rem;
width: 1rem;
position: absolute;
z-index: 2;
}
.controls {
position: fixed;
bottom: 4px;
right: 4px;
text-align: right;
}
<div id="frame"></div>
<div class="controls">
<button id="measure">
measure
</button>
<button id="randomize">
spawn cubes
</button>
<div>
N cubes:
<input id="nCubes" type="number" value="40">
</div>
<div>
Cube size:
<input id="size" type="number" value="16">
</div>
<div>
N inside large:
<span id="result">
</span>
</div>
<div>
Time (ms):
<span id="time">
</span>
</div>
<div>
Points:
<span id="points">
</span>
</div>
<div>
N inside large (loop):
<span id="result-loop">
</span>
</div>
<div>
Time (ms) (loop):
<span id="time-loop">
</span>
</div>
</div>
In my original work I had a <div class="cursor"></div> with this styling:
.cursor {
pointer-events: none;
position: fixed;
padding: 0.7rem;
background-color: #fff;
border-radius: 50%;
mix-blend-mode: difference;
transition: transform 0.3s ease;
}
In the script section of my HTML file I have this javascript function for my cursor animation:
(function () {
const link = document.querySelectorAll('nav > .hover-this');
const cursor = document.querySelector('.cursor');
const animateit = function (e) {
const span = this.querySelector('span');
const { offsetX: x, offsetY: y } = e,
{ offsetWidth: width, offsetHeight: height } = this,
move = 25,
xMove = x / width * (move * 2) - move,
yMove = y / height * (move * 2) - move;
span.style.transform = `translate(${xMove}px, ${yMove}px)`;
if (e.type === 'mouseleave') span.style.transform = '';
};
const editCursor = e => {
const { clientX: x, clientY: y } = e;
cursor.style.left = x + 'px';
cursor.style.top = y + 'px';
};
link.forEach(b => b.addEventListener('mousemove', animateit));
link.forEach(b => b.addEventListener('mouseleave', animateit));
window.addEventListener('mousemove', editCursor);
})();
How do I change this so it can work within React?
This is a very broad question but here are a few pointers to get you started...
Start with the React tutorial available from the react site itself at reactjs.org/tutorial/tutorial.html. It will cover the basics and also many 'react-y' programming patterns and best practices.
Once you've got that there are a huge number of tutorials available that go much more in depth. I would highly recommend Robin Wieruch's stuff: https://www.robinwieruch.de/ (he has also written a number of books on the subject)
Looking at your problem more closely. I would start by creating a component for the cursor and moving the bulk of the js to that. The problem is that without a good grounding in React (or at least the fundamentals) the question you're actually asking is 'How to program in React' rather than 'How do I do this particular thing in React' which would take a lot longer to write an answer to than most of us here on SO have time to do!
I am creating a canvas alike without using canvas tag , by creating new div each time mouse is down, i can't figure out how to run a second event.
Like mousedown then mousemove event where the seconed event occur only after the first one is true?
also if you can help with the offset coordinates
var paintbox = document.getElementById("canvas");
var start = function() {
paintbox.addEventListener("mousedown", drawOnCanvas);
};
var newColor = document.getElementById("colorPick");
var drawOnCanvas = function() {
var newClick = document.createElement("div");
newClick.setAttribute("id", "smallDiv");
newClick.style.backgroundColor = newColor.value;
newClick.style.width = "10px";
newClick.style.height = "10px";
newClick.style.position = "absolute";
paintbox.appendChild(newClick);
}
Set the handler on the mousemove event instead of mousedown and in the handler check whether the mouse button is down.
It would be better to move the 10px/position style settings in a CSS class. Also, don't generate elements with the same id attribute value: that generates invalid HTML. Use a class instead.
For the positioning you can use the pageX and pageY properties of the event object.
Finally, as the events will be triggered on the small divs when you make small moves, an extra check might be necessary to verify the mouse is still in the paint box.
var paintbox = document.getElementById("canvas");
var start = function() {
paintbox.addEventListener("mousemove", drawOnCanvas);
};
var newColor = document.getElementById("colorPick");
var drawOnCanvas = function(e) {
if ((e.buttons & 1) === 0) return; // Mouse button is not down
// Extra check to see we are well within the box boundaries:
var box = paintbox.getBoundingClientRect();
if (e.clientX - 5 < box.left || e.clientX + 5 > box.right
|| e.clientY - 5 < box.top || e.clientY + 5 > box.bottom) return;
var newClick = document.createElement("div");
newClick.className = "smallDiv"; // Don't create duplicate ID; put CSS in class
newClick.style.backgroundColor = newColor.value;
paintbox.appendChild(newClick);
newClick.style.left = (e.pageX-5) + "px";
newClick.style.top = (e.pageY-5) + "px";
}
start();
#canvas {
height: 150px;
width: 300px;
border: 1px solid;
display: inline-block;
margin: 10px;
}
.smallDiv {
width: 10px;
height: 10px;
position: absolute;
}
Color: <input id="colorPick" type="color"><br>
<div id="canvas"></div>
What I'm trying to do (which I've been able to accomplish, but with poor performances) is to apply a sort of grid over the canvas, in order to be able to take inputs from the user about the origin point position. Once the input is received, the "draw" coordinates are provided via keyboard.
What I've managed to do in these days, was to calculate the width and height of the canvas, then divide it by the area of a standard 20x20 square (speaking in px). In this way I can loop on the result and create n squares, that I will render in display flex inside the grid element. Then this grid element is applied "over" the canvas.
Everything works, but there's a lot of divs going around, and if the user choses to shrink the div to let's say 10x10, then, that would have a great impact over the performances... So I'm trying to find out a lighter way to do this...
I've thought about using HR elements inside two divs that would be applied over the canvas. One div displays elements in column, and another in row. In this way I should obtain the grid, but what about the snap? How could I detect the intersection on the two HR elements and use that exact spot as position?
The reason of why I cannot directly draw the grid on the canvas is because this should remain as 'pure' as possible. Containing only the final draw of the user.
Here's the 'non optimized' code:
I'm using Angular 5 as framework.
<div class="draw-zone" #drawZone>
<div class="grid" #grid [ngClass]="{'activated': activateDrawZones}">
<div *ngFor="let block of gridBlocks" class="grid-block" [ngClass]="{'show': showGrid, 'ten-x-ten': blockSize === 10, 'twe-x-twe': blockSize === 20, 'thr-x-thr': blockSize === 30, 'fou-x-fou': blockSize === 40}"
#gridBlock (click)="draw($event, gridBlock)"></div>
</div>
<canvas #canvas [height]="canvasSize.y" [width]="canvasSize.x"></canvas>
</div>
The scss:
.draw-zone{
flex-grow: 2;
height: 100%;
position: relative;
canvas{
z-index: 10;
}
.grid{
top: 0;
left: 0;
z-index: 11;
width: 100%;
display: flex;
flex-wrap: wrap;
overflow: hidden;
position: absolute;
margin-left: -.1rem;
border-radius: .5rem;
align-content: stretch;
border: 1px solid transparent;
&.activated{
border-color: #3f51b5;
}
.grid-block{
opacity: 0;
border-right: 1px solid #3f51b5;
border-bottom: 1px solid #3f51b5;
&.show{
opacity: .1;
}
&:hover{
opacity: 1;
border-radius: 50%;
background-color: #3f51b5;
transform: scale(1.2);
}
&.ten-x-ten{
width: 10px;
height: 10px;
}
&.twe-x-twe{
width: 20px;
height: 20px;
}
&.thr-x-thr{
width: 30px;
height: 30px;
}
&.fou-x-fou{
width: 40px;
height: 40px;
}
}
}
And the component method to cal:
private calculateGrid() {
this.activateDrawZones = false;
this.canvasSize.x = this._drawZone.nativeElement.clientWidth;
this.canvasSize.y = this._drawZone.nativeElement.clientHeight;
const blocksCount = (this.canvasSize.x * this.canvasSize.y) / (this.blockSize * this.blockSize);
this.gridBlocks = [];
for (let i = 0; i < blocksCount; i++) {
this.gridBlocks.push({ size: this.blockSize });
}
this.activateDrawZones = true;
}
And the method that actually draws:
public draw(e: MouseEvent, block: HTMLDivElement, returnOnFail?: boolean) {
const x = block.offsetLeft + (this.blockSize / 2);
const y = block.offsetTop + (this.blockSize / 2);
if (this.firstClick) {
this.ctx.beginPath();
this.ctx.moveTo(x, y);
this.setCrosshair(x, y);
this.firstClick = false;
this.addPathToDrawSequence(x, y);
return;
}
if (this.isNotOnTheSameAxisAsTheLastInsert(x, y)) {
if (returnOnFail) { return; }
this.toggleDrawDirection();
this.draw(e, block, true);
return;
}
this.ctx.lineTo(x, y);
this.ctx.stroke();
this.setCrosshair(x, y);
this.addPathToDrawSequence(x, y);
}
As you can see, I'm applying the '.grid' element over the canvas element. The grid element contains all the blocks that are displayed in flex mode. As you can see the grid container has a display:flex and flex-wrap: wrap properties. In this way, when the user clicks over a block, I can guess the x, y coordinates by getting its position, relative to the parent. Which has the same dimensions as the canvas. Once that I have the x,y coords, i can draw on the canvas.
Yes, creating a multiplicity of DOM elements and trying to dynamically position and size them with javascript will not be particularly performant. I don't think hr elements will solve this problem for you.
First, have you considered drawing your grid directly onto the canvas?
Another option is to have a background image with the grid on it layered behind the canvas. This will automatically resize just as performantly as any other aspect of your webpage.
Now for the 'snapping' part. It looks like you've already figured out how to draw what you need on the canvas once you get the grid information you're looking for. What you need is a method to get which grid a user clicked on. I'm guessing that is why you overlaid all those divs...
Instead, canvas natively tracks mouse clicks. Using some techniques laid out here should be able to get you the grid interaction information you're looking for.
Edit: A method to generate and find grids:
var height = 100;
var width = 200;
var horizontal_grids = 8;
var vertical_grids = 4;
function bounding_grid_1d(length, grids, x) {
var divisions = [];
var grid_width = length / grids;
for ( i = 0; i <= grids; i++ ) {
if (x || x == 0) {
if (i*grid_width > x) {
divisions.push((i-1)*grid_width);
divisions.push(i*grid_width);
break;
}
else if (i*grid_width == x) {
divisions.push(i*grid_width);
break;
}
}
else {
divisions.push(i*grid_width);
}
}
return divisions;
}
console.log("Get all the x and y grid line locations");
console.log(bounding_grid_1d(width, horizontal_grids));
console.log(bounding_grid_1d(height, vertical_grids));
console.log("Get the x and y grid line locations that surround the coordinates (60,30)");
console.log(bounding_grid_1d(width, horizontal_grids, 60));
console.log(bounding_grid_1d(height, vertical_grids, 30));
I am looking to select items in a web page using the LeapMotion and I am struggling with programming this interaction.
Using this code as a base (but updating the link so it connects to the current SDK) I can get my cursor to move around the window based on where my hand is in space. However, I do not know how to make the equivalent of the event listener "click" using the LeapMotion. I am using Leap.js and have built a crude GUI using svg.js.
How do I program an event listener that selects using the LeapMotion in Javascript?
I have working code with Leap motion. I did quize software. Here cursor is a div element which styled as:
#cursor {
width: 60px;
height: 60px;
position: fixed;
margin-left: 20px;
margin-top: 20px;
z-index: 99999;
opacity: 0.9;
background: black;
border-radius: 100%;
background: -webkit-radial-gradient(100px 100px, circle, #f00, #ff6a00);
background: -moz-radial-gradient(100px 100px, circle, #f00, #ff6a00);
background: radial-gradient(100px 100px, circle, #f00, #ff6a00);
}
and Java script at bottom. What I did, I get HTML element by position and triggering click event on it by tap gestures.
var w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
// Setting cursor and output position
window.cursor = $('#cursor');
var controller= Leap.loop(function(frame) {
if (frame.pointables.length > 0) {
try
{
var position = frame.pointables[0].stabilizedTipPosition;
var normalized = frame.interactionBox.normalizePoint(position);
var cx = w * normalized[0];
var cy = h * (1 - normalized[1]);
$('#cursor').css('left', cx);
$('#cursor').css('top', cy);
}catch(e){
console.error(e);
}
}
});
controller.use('screenPosition', {
scale: 1
});
controller.on('gesture', onGesture);
function onGesture(gesture,frame)
{
try
{
// If gesture type is keyTap
switch(gesture.type)
{
case 'keyTap':
case 'screenTap':
var position = frame.pointables[0].stabilizedTipPosition;
var normalized = frame.interactionBox.normalizePoint(position);
//Hiding cursor for getting background element
cursor.hide();
// Trying find element by position
var cx = w * normalized[0];
var cy = h * (1 - normalized[1]);
var el = document.elementFromPoint(cx, cy);
cursor.show();
console.log(el);
if (el) {
$(el).trigger("click");
}
break;
}
}
catch (e) {
console.info(e);
}
}