Draggable objects
redblobgames.com
redblobgames.com
One of the hardest things I have needed to code from scratch is drag-to-reorder. It seem so natural from a user perspective, but when you get into inconsistently sized items, having to create placeholders between items, detecting edges, going down rabbitholes of box-fitting algorithms... it's a fun challenge :)
1. When a user begins dragging, calculate the layouts for all possible drop targets (or perhaps just those that are currently visible). 2. For each of those layouts record the position the dragged objects ends up in. 3. On each mouse movement, select from those positions the one that's closest to the dragged object's current position 4. Render the selected layout
This ends up feeling really good and works for any kind of complex layout / reflow.
> TypeError: input.getBoundingClientRect is not a function
Can you explain this in more detail? Don’t you need to actually change the layout to figure this out correctly? Or are you approximating it with the top/left corner of the current element in the same place or something?
EDIT: I read to fast, for some reason I understood step 1 as figuring out the location/bounding box of each drop target, but I think you mean to actually put the dragged object there and let the browser compute the layout.
What I ended up doing is when you pick up a card, I compute the layout as if the card was deleted from the board, and then it becomes easy. Wherever you hover the mouse, I just displace whatever is there.
There were still tons of edge cases I had to work out, though, especially when you start editing a new card that hasn’t been “committed” to the data model yet. I had to add the option to shift existing cards out of the way to make room for a phantom card.
It helps to recognize that there are just lots of edge cases you have to manually handle. If you try to tackle it as though there’s a more generic/homogeneous solution, you end up going around in circles a bit with the design. I should probably create a blog post on all the different edge cases.
As I said, though, I found an even better way to do my layout, which saves on unnecessary computations and makes the layout engine more flexible and user-friendly. (It’s amazing what a difference your choice of data model representation makes on your solution.) It’s been a fun puzzle to solve!
In the case of cancellation-when-dragging-outside-an-area, there’s also un-cancellation, meaning you resume the dragging when the pointer returns to the area, after the state visually reverted to the original one while outside the area (to indicate to the user that a cancellation would happen if the mouse button is released at that point). Or put differently, the real cancellation only happens upon mouse-up, but is already visually indicated while dragging.
But that Windows <<SNAP>> when you drag something too far so the dragged object and the pointer just suddenly disappears, and snaps back to the original position without warning!
Can’t believe Microsoft still believes that is smart UI design. It utterly confuses users. You may even see them caught in a yo-yo situation or just scared to release the left mouse button while they don’t know what’s happening
Cancellation is a huge often overlooked issue - I agree with you and it’s not a simple issue. Common users don’t know that ESC can help.
It becomes massively simpler conceptually (and even in the code) to treat any button press as a potential drag, and a simple click becomes just a drag-with-no-movement.
A: button press, button release, button press, button release
B: button press, button press, button release
This subtle difference leads to a world of difference in mid-level code.
Option B allows to implement an “incompatible” single-click action, with the trade-off that it must be bound to the button release, not the (first) button press.
It involved dragging.
I loved the 2am conversations that resulted. My idea was that dragging need not respect real-world physics. Dragging should feel like a great tab of acid. And everyone was into this, everyone had ideas.
I once wanted to make a customizable Pomodoro timer UI based on subdividing a circular clock into wedges of different durations to define your focus/break intervals. I didn't get very far trying to implement drag-to-reorder of the wedges.
I wanted to separate the constraint system from the event handling system. Libraries like jquery-ui tie them together, so the event handling system has to know all the possible constraints. In jquery-ui, they support bounding box, axis, square grid, snapping to dom elements. But what if I wanted snapping to a hex grid, or a logarithmic scale grid, or a bounding circle? It's not supported.
In the code you'll see "state.pos = …". That's where the state is set. For constraints, I put "pos" behind a setter. Then the setter can apply the constraint, without the drag event handling code having to know what type of constraints are needed.
I should update the page to show some examples of constraints. I completely forgot to mention this aspect of the code recipe. (Thanks!)
I have some older examples of constraints at https://www.redblobgames.com/articles/curved-paths/making-of... and a prevent-collision example at https://redblobgames.github.io/circular-obstacle-pathfinding... . However I haven't tried drag-to-reorder or animated drop targets.
For something more "out of the box", I've been using interactjs for quite a while for a variety of my projects
Definitely one of the best websites I know. Cheers!
What's your most interesting piece that hasn't gotten sufficient attention yet? I believe HN has had many great threads about the A* and hexagonal grid articles over the years. Is there a comparable one that's been overlooked so far?
I think the best candidate is https://www.redblobgames.com/making-of/draggable/ . It's from earlier this year and hasn't been posted to HN yet (I think).
[1] https://simblob.blogspot.com/2023/04/explaining-hexagon-layo...
[2] https://simblob.blogspot.com/2023/04/improving-mapgen4s-boun...
Obviously, many comments on the thread predate this change, but there's enough context here for readers to figure that out, and hopefully we can get a more specific discussion going.
Generally I feel like links to collections of stuff do well when they are interesting and don’t when they aren’t and that you modding to a specific example isn’t actually improving quality.
But if we're to optimize HN for intellectual curiosity (https://hn.algolia.com/?dateRange=all&page=0&prefix=true&sor...), we have to consider thread quality, and there's no doubt that submissions like this generally lead to generic, and therefore shallow, discussion in the way that I described upthread.
Overall I think the best way for HN readers to discover a site like this is bottom-up: to run across an example of a great article and a great thread about it, and then click around to discover what else is there. This is more in the intended spirit of HN.
Edit: it's a little unorthodox for us to change the URL in midstream after a submission has this many upvotes and pre-existing comments, but I hope everyone understands that I did so to give the site more exposure and appreciation, not less. The alternative would have been to downweight the post as a "list submission" (https://news.ycombinator.com/item?id=37707904), and I didn't want to do that.
Coming back to the thread half a day later now, I suddenly found that I upvoted the page "Draggable objects" despite never having previously visited or hearing of the page.
I read the new page anyway and liked it. But it contradicts the original my intent of the upvote and essentially gaslights me into a fictitious past.
I've a quick question. How do you restrict the dragging movement to an axis using the built-in DOM events like dragstart/etc. I had a drag & drop feature implemented using the dragstart/dragenter/dragover/drop/etc events. I couldn't find a quick way to restrict the dragging movement to the x-axis. JQuery's drag and drop API used to support it. I'm trying to use the native DOM events/api only. Any information or pointers are greatly appreciated.
1. When I get the event, I update the underlying state, but leave the DOM element alone.
2. I can apply constraints to that state. To restrict dragging to the x-axis, I would never change y.
3. I use the state to drive the redisplay of the DOM element.
Some examples of constraints I want: https://www.redblobgames.com/articles/curved-paths/making-of...
I think jQuery's drag and drop API is using the mouse events directly and not using the built-in dragstart/etc., and that's why they can apply constraints.
Are there shortcomings with the built-in feature compared to your code?
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement...
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement...
https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att...
> Note that this is not the HTML Drag and Drop API, which involves dragging an element onto another element. For my diagrams, I’m dragging but not dropping, and the scrubbable number example shows how I’m not necessarily even moving something around. So I need to read the mouse/touch events directly.
The drag and drop API is doing a different thing that isn't always appropriate - you can get it to do roughly what this article is talking about by making the thing being dragged invisible just after the dragging starts, but it can shrink things and doesn't let you edit them as DOM nodes after the dragging has started. It also lets you drag something out of the browser window and try to put it into a completely different window.
Big fan of your writing, btw!
The longer answer is: I'm not actually sure how to solve this problem. The second site, simblob.blogspot.com, is an actual blog. It's time ordered. It has posts. But the main site, redblobgames.com, is structured as a "living document" site, not as a blog. Things are not posted in order.
I could try an automated feed that looks for any changes on pages. But I make small changes all the time. For example on [1] I changed "So far we’ve made step have the same" to "So far we’ve made steps have the same". I've been fixing a lot of broken links and typos this week, so there are lots of pages that have changed, but not in meaningful ways. I don't want those showing up on the RSS feed.
The second problem with an automated feed is that I have pages that aren't meant for publication. I collect notes for myself before I write an article, sometimes for years. For example [2] is something I may never get around to publishing. I don't want new pages to show up on the RSS feed, because most of them aren't ready yet, and some may never be.
So an alternative is for me to manually add entries to an RSS feed when they're "meaningful". I'm trying to do this by posting to the simblob.blogspot.com blog. An example is [3] where I describe the changes I'm making to the mapgen4 page. These wouldn't have been picked up by an automated feed because the HTML didn't change, but the interactive part did, so I wrote about it. Another example is [4] which is about changes to the hexagon page. I'll also go into a lot more details about why I made those changes [5].
But manually writing blog posts means there will be changes that won't show up in the RSS. Ideally I'd have some kind of automated way to flag meaningful changes to the site, but until then I am trying to write meaningful changes on the blog.
[1] https://www.redblobgames.com/pathfinding/a-star/introduction...
[2] https://www.redblobgames.com/articles/probability/loot-drops...
[3] https://simblob.blogspot.com/2023/04/improving-mapgen4s-boun...
[4] https://simblob.blogspot.com/2023/04/explaining-hexagon-layo...
[5] https://simblob.blogspot.com/2022/11/introduction-to-hexagon...
Do you worry at all about someone’s browser not supporting them, given that Safari added support in 2020? I guess Safari 12 is hopefully no longer used in practice, with macOS Mojave users hopefully running Safari 13 or 14? It would be pretty bad if something as simple as dragging didn’t work in a production app designed for the market of web users at large.
Adding event handlers to the document during a drag is a time-honored practice, and browsers add brand-new features all the time that are intended to simplify some use case or other but have their own edge cases and gotchas, which the article says are not fully addressed. And there’s still a combination of pointer and touch events in the end result. I wonder if the “simplicity” in the sense of less code is worth the additional edge cases, less browser support, and the developer needing to understand the ins and outs and browser differences of pointer events, which are presumably less understood and documented than mouse events.
This page is not prescriptive: I'm not trying to tell everyone else what to do. This page is descriptive: I'm trying to document what works for me. For the use cases I need on my pages, pointer events cleared up a lot of glitches and edge cases I previously had with mouse+touch events. There are some that remain. I'm happy with the switch. This isn't a luxury everyone has. Not all use cases are as well supported as the things I want to do.
In my example, the answer to the question about accessibility is to additionally provide keyboard controls to complete the task (preferably ones that don't - or optionally don't - require holding a key). For example perhaps a key shortcut to enter a pan mode, in which the arrow keys move the viewport around the canvas. Problem solved.
As for draggable objects, the task is re-ordering. How do you make that accessible? Provide an alternative means to re-order objects, perhaps using TAB to cycle focus through the objects, then a key to select the focused one, and use the arrow keys to move the drag preview to the nearest valid position in that direction.
I was just implementing dragging an SVG element in a Vue app earlier this week, and had to discover pretty much everything the author describes in the article, even in the same order the author describes them, and ended up with pretty much an identical component to do so (except I wrote a composable utility `useDragging` instead of a functional component `<Draggable>`).
Yes, the Perlin noise guy among many other things.
In addition to range I'd like to see the distribution, especially with multiple octaves of noises added together. I haven't found a good page about that.
Not super precise, but given a 2D boolean array of pathable/unpathable cells, say generated by Perlin noise, find locations that are only accessible via a relatively narrow "choke point". Example: https://imgur.com/a/jFPXlS5
Standard pathfinding algorithms don't provide enough information to do this, but maybe there's some kind of heuristic approach that could work well.
Choose random starting point and compute distance to all other points on the map. Repeat for multiple random starting points.
Average the distance.
Points with high average distance are difficult to reach.
You might be luckier treating this as a "map treatment" problem. An algorithm that does things to the whole map, and then reads the result.
For example:
1. Start assigning a score of 0 to all map cells.
2. For every cell in the map, set to 1 if it's in contact with any walls
3. Then add 1 to every cell of the map if it contacts a cell with a non-zero value
4. Repeat the above step n times, where n is the average "radius" of you enclave rooms.
5. Every cell with a score of n or higher is a "candidate". For every candidate:
5.a Check that none of the cells around have a bigger score. If so, move on to the next candidate
5.b Check that there's no "treasure" around it in a circle of radius n
5.c You have found the center of an enclave. Mark it with "treasure" and move on to the next cell.This is one way to approach the problem. The other way is to do prefab rooms and when generating your dungeons, randomly select one or two prefab enclave rooms to throw into the shuffle. Shuffle the rooms and spread them out then connect hallways and such. This is the approach that Enter the Gungeon took.
Another approach is what @otikik describes. Tracing the walls buy assigning a value to the cells that can then be scored. Minesweeper style.
[1] https://old.reddit.com/r/roguelikedev/comments/dc4orn/identi...
[2] https://www.redblobgames.com/pathfinding/distance-to-any/
I am collecting some of the design elements here: https://www.redblobgames.com/making-of/little-things/
One of the seven evil hexes :D
Regular tech blog: https://amitp.blogspot.com/ Game Dev blog: https://simblob.blogspot.com/
[1] https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode
- The Wiki pseduocode uses Set data structures for everything. While these are covered in classes, the up-tree concept isn't as heavily described as other trees
- Looking at it now, it looks like the pseudocode is a little more beginner friendly, but "back in mah day" it was not
- Although, RedBlob's pseudocode for obtaining neighboring nodes is clearer than "for each neighbor of current" (Wikipedia pseudocode)
- RedBlob uses Priority Queues and Maps instead of Sets, which connect better to other AI searches / recommendation algorithms. Higher 'priority' recommendations move to root in PQs while nodes in Sets... don't... they just sort of 'exist'
- The Maps make looking up node costs more intuitive than lists (aka "current := the node in openSet having the lowest fScore[] value")
[2] https://www.redblobgames.com/pathfinding/a-star/implementati...
2. The textbooks use an "open" and "closed" set. But in code, these aren't explicitly stored in set data structures. Instead, they're implicit. The cost_so_far dict(map) contains as keys both the open and closed sets, and the frontier (priority queue) contains the open set. So in my explanation of A* I focus on these data structures (priority queue and dict) instead of the open/closed sets. And when I do talk about the sets, I talk about the combined open and closed sets, calling it "visited" or "reached", because it's the combined set that is actually in the data structures.
3. The textbooks use a priority queue with reprioritization. When you visit a node that has a lower cost than the previously found cost, you go into the priority queue and adjust the cost. In my presentation I don't use reprioritization. Instead, I insert another entry into the priority queue with the lower cost. This makes the priority queue simpler (reprioritization is complicated). And in practice, I think it's faster too.
For posterity, here's Amit on map generation: https://simblob.blogspot.com/2010/01/simple-map-generation.h...
I’ve also always enjoyed every (virtual) interaction I’ve had with him, however brief — and appreciate that he remembers me and the problems/challenges we faced in my games.
SimAirport and SimCasino very likely wouldn’t be the same (or would have taken much longer to achieve, at best) without his content.
Just a class act; one of these days I’d love to buy Amit a beer or a coffee. My invite doesn’t expire, perhaps next time you’re in the area! :)
Anyone has some more recommendations of sites with similar content (easily explained algorithms with great interactive visualizations? )
Look into Montecarlo tree search, CFRM, AB pruning, and also more recent deep learning methods.
It's a very exciting area of research
I think a lot about some advice I heard from the creator of Brogue. Essentially players have a tough time figuring out how AIs make their decisions and often assign complex motives to them when they don't exist. His example was he coded archers to try to maintain a position in some range band from the player as their primary motivation in a vacuum. The community would assign all sorts of supposed logic to archer behavior because in real-world environments with extra geometry or extra situational AI routines they couldn't see behind the curtain. Additionally, that most game AI exists to create a fun experience for the player, we only make it try to play well in service of that goal.
Amit's work is awesome! Right up there with Inigo Quillez's site on graphics in terms of informativeness and fascination in his own domain.
I once tried throttling updates via requestAnimationFrame but it didn't help much. Perhaps movement prediction?
Maybe you could try turning the actual cursor invisible when dragging and instead render a custom drag cursor parented to the object being dragged.
[1] https://www.redblobgames.com/articles/probability/damage-rol...
Consider a game like Age of Empires. In the beginning you might be able to defend your village by building walls around it. For an attacker, there is a high cost to go through the wall. But when cannons are invented, suddenly the cost of going through the wall drops.
Is there a way to model this? Can you draw a graph of the terrain that looks different before and after the invention of cannon?
Lists don't make great HN submissions because they don't point to anything specific enough. The resulting discussion tends to be generic and shallow, because the only material to discuss is the lowest-common-denominator, i.e. whatever the items on the list have in common.
This thread is a nice example because the comments are only about the site in general—mostly how good it is—which of course is true, but not 'interesting' (in HN's sense of the word) enough to devote a frontpage slot to (https://hn.algolia.com/?dateRange=all&page=0&prefix=true&que...).
The solution is to pick the most interesting element from the list and submit that instead. Maybe we should ask amitp to nominate one :)
Edit: I did that and have changed the URL now. See https://news.ycombinator.com/item?id=37708834. (I will also mark the current comment off topic so it goes to the bottom of the page.)