-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Suggestion for the submarine component #1
Comments
Hi Maaz and Jason, I ended up toying a bit with this as well, but my Javascript (; and certainly DOM/CSS) skills are somewhat lacking, so I used the language I happen to know, ReScript. The code compiles and type checks, so there's likely to be some arithmetic bugs in there, but the general structure of the code should hold. It doesn't take that much more effort to turn it into a React component that would be possible to hook from JS though. First, we write a way to do 2d-linear algebra on vectors. And we define a way to transform the submarine and directional commands into a Next, we define a way to turn a DOM element into a Finally, define an Area as a box you can be either inside or outside, noting that they are complementary: you are outside a box if you are not inside it. A With this in place, and a bit of glue, it essentially implements the core logic, but it would need more work to embed inside a React component because it doesn't care about Oh, and note this was mostly just to have fun with this. I got the transformation idea and wanted to see how hard it would be to write. And how many more lines it would take over doing it directly. I ended up spending far more time on figuring out how you access CSS/DOM from ReScript, than the rest of the code which was somewhat straightforward. So in the end, I ended up learning something about CSS and the DOM in the process :) (The Javascript the rescript compiler generates is quite readable. I'd happily attach it if you want to study what the compiler does). /* Direction defines the directive commands we can supply to the system
* In a more real setting this would be an action type in Rescript/React,
* and would also contain the "pet" command. But it suffices for this
*/
type direction = Up | Down | Left | Right
// 2d Vectors
module Vec = {
// The type of a vector
type t = {x: int, y: int}
// Create a new vector out of a pair.
let make = (~x, ~y) => {x: x, y: y}
// You can multiply vectors by a scalar, and you can add them
let scale = (s, {x, y}) => {x: s * x, y: s * y}
let add = ({x: x1, y: y1}, {x: x2, y: y2}) => {x: x1 + x2, y: y1 + y2}
/* Compute a unit direction vector from a direction */
let fromDirection = d =>
switch d {
| Up => {x: 0, y: -1}
| Down => {x: 0, y: 1}
| Left => {x: -1, y: 0}
| Right => {x: 1, y: 0}
}
}
// Type of boxes
module Box = {
// A box is an upper-left point and a width + height
type t = {
ul: Vec.t,
width: int,
height: int,
}
// Not strictly necessary. But factoring box creation through this function
// allows us to change the type above and reorder how we want to compute coordinates
// later on if need be. We can force this by making `t` above abstract via
// a module type later.
let make = (~offset, ~width, ~height) => {ul: offset, width: width, height: height}
// Predicate. Is p inside the box? We are assuming coordinates are relative to the window.
let inside = (p: Vec.t, box: t) =>
p.x > box.ul.x && p.y > box.ul.y && p.x < box.ul.x + box.width && p.y < box.ul.y + box.height
// Create a box out of a dom element
let fromElement = e => {
let rect = Webapi.Dom.Element.getBoundingClientRect(e)
let x = rect->Webapi.Dom.DomRect.x->Belt.Float.toInt
let y = rect->Webapi.Dom.DomRect.y->Belt.Float.toInt
let width = rect->Webapi.Dom.DomRect.width->Belt.Float.toInt
let height = rect->Webapi.Dom.DomRect.height->Belt.Float.toInt
make(~offset=Vec.make(~x, ~y), ~width, ~height)
}
}
// Generalize boxes to safe areas. This allow you to bundle boxes together
// to form where it's safe to move the submarine.
module SafeArea = {
// An area is either Inside-the-box or Outside-the-box
type area =
| Inside(Box.t)
| Outside(Box.t)
// A SafeArea is an array of areas
type t = Js.Array2.t<area>
// Construct areas. Either inside or outside
let makeInside = box => [Inside(box)]
let makeOutside = box => [Outside(box)]
// If you have several safe areas, you can combine them into a union
let union = Js.Array2.concat
// A point p is safe if it is faithful to all the areas we have
let safe = (p, areas) => {
// valid analyzes a single area
let valid = area =>
switch area {
| Inside(box) => Box.inside(p, box)
| Outside(box) => !Box.inside(p, box)
}
areas->Js.Array2.every(valid)
}
}
// Submarine helpers, just bundled up in a module so we can name-and-conquer.
module Submarine = {
// Obtain a position from a Dom element. Turn it into a Vec if possible
let position = (~submarine, ~window) => {
let style = Webapi.Dom.Window.getComputedStyle(submarine, window)
let l = style->Webapi.Dom.CssStyleDeclaration.left
let t = style->Webapi.Dom.CssStyleDeclaration.top
switch (Belt.Int.fromString(l), Belt.Int.fromString(t)) {
| (Some(x), Some(y)) => Some(Vec.make(~x, ~y))
| (_, _) => None
}
}
// Given a style, set its position. A real solution also needs to care
// for transform.
let setPosition = (~style, ~pos: Vec.t) => {
let set = (id, v) =>
Webapi.Dom.CssStyleDeclaration.setProperty(id, Js.Int.toString(v) ++ "px", "", style)
set("left", pos.x)
set("top", pos.y)
}
}
// Try to move the submarine from current position.
// Turn the move into an optional value where the return is Some(p) if
// the move is possible.
let tryMove = (~wrapper, ~currentPos, ~move) => {
// Utilize our helper modules to compute this
let box = Box.fromElement(wrapper)
let safe = SafeArea.makeInside(box)
let newPos = Vec.add(currentPos, move)
switch SafeArea.safe(newPos, safe) {
| false => None
| true => Some(newPos)
}
}
// Compute a new possible position for a submarine.
let computeMove = (~submarine, ~scale, ~direction, ~wrapper, ~window) => {
let move = Vec.scale(scale, Vec.fromDirection(direction))
switch Submarine.position(~submarine, ~window) {
| None => {
Js.log("Expected to find a submarine style, but none was found")
None
}
| Some(currentPos) => tryMove(~wrapper, ~currentPos, ~move)
}
}
let movement_distance = 10
// To set the submarine, use this function
let setSubmarine = (~submarine: Webapi.Dom.Element.t, ~direction, ~wrapper, ~window) => {
let scale = movement_distance
switch computeMove(~submarine, ~direction, ~wrapper, ~scale, ~window) {
| None => // Movement isn't possible, so don't do anything
()
| Some(pos: Vec.t) =>
// Update the submarine position
switch Webapi.Dom.HtmlElement.ofElement(submarine) {
| None => Js.log("Could not find submarine in HTML Dom")
| Some(e) => {
let style = Webapi.Dom.HtmlElement.style(e)
Submarine.setPosition(~style, ~pos)
}
}
}
} |
@maazadeeb this is great! your solution is really nice — I'd love a PR! @jlouis this is very cool as well! I love seeing another solution. I probably don't want to add it for this project, since I don't use Rescript, but I really appreciate seeing how you solved it! |
@jlengstorf It's the sensible move not to start relying on Rescript I think, and I didn't expect it to be included as well. It was more like a FYI, now I had written it, and I thought the solution was interesting enough that more people should see it. Over the weekend, looking at it now, there's a couple of changes that should definitely be considered:
|
@jlengstorf I had an idea when watching the stream. I couldn't get the repo to run on my system, so I created a codesandbox with most of the code from
submarine.js
.The idea is to have a 2D array of the directions, and use them to calculate the new left and top positions. Also, I added direction for the top/down commands as well.
If you think this works well, I could submit a PR. I didn't know how else to ask, so I created an issue. :)
The text was updated successfully, but these errors were encountered: