Wednesday, November 30, 2022

determining if angle is inside arc on a circle...

Yesterday (November 29, 2022) was the video game Pong's 50th Birthday! Not the first video game but the first to really take off in the public imagination.

To celebrate, I made an (not very) original game: Rotato Solo Pong

You can mess with the program online. (The version on my site adds a little code to help with mobile, so the finger control doesn't drag the whole screen around....)

I used to make a lot of online games and toys but I haven't been lately... going over some of the cool stuff with my niece made me think I should get back into it a bit? It's a very pleasurable and satisfying hobby, even if I don't know how to publicize a game to get it in front of more people...

This one was trickier than I expected - specifically, figuring out if the ball was hitting the paddle, which I was content with saying "is the angle the ball is at relative to the circle (easily calculable with atan2 function) inside the edges of the paddle, when the ball is on the circle border (easily calculable with the dist() function)" 

At the time of development, it was two paddles - a cooler visual but less fun game mechanic, since you barely had to move to hit the ball...

So circles go from 0 degrees to 2 * PI... and then they start over (at 3 o'clock on a clockface.)  The trouble occurs at that zero = 2 * PI boundary - like when a paddle is perched with one end on either side, it's tough to figure out what angle values to compare vs the angle of the ball.

To work my way through it, I set up a lot of visuals within the game - click here for an interactive recreation to show the problem - an exclamation point shows up when it has detected an overlap of the circle with either paddle:

This shows my first attempt at normalizing angles (like adding 2*PI if the angle was negative, for example) It shows the basic double rotatopong board, with color coded displays  below of the linear values I'm trying to compare to figure out the overlap. You can see that the blue paddle is near that troublesome boundary bit, and so the visual segment of the endpoints (shown underneath) is elongated, and tilted the "wrong direction" 

It was frustrating that this problem was as hard as it was - in fact I reached out to a friend who works at Google but I ended up solving it before he got back to me. One trick was this: for the paddles, I couldn't normalize the angle one at a time: if I had to make an adjustment, it needed to be on both ends of the paddle, so to speak.

That prevented the "weird elongated bar" problem, but there were still misses. My final technique normalizes all angles, but then uses a duplicate of the ball angle - i.e. comparing both the ball angle AND ball angle + 2*PI to see if either is inside the especially normalized paddle end points.

So I think that works? I don't know if there's a more sane way of doing it though. It seems like the problem should be straight forward, but the whole thinking radially is actually pretty tricky...

(I tackled a similar and seeming more difficult problem way back when, figuring out which direction to turn for a heatseeker effect.)



Wednesday, November 23, 2022

javascript translate urls into links, but skipping things that are already links...

A while ago I posted a snippet of linky, and explaining how "changing the current highlighted text into a link" (heuristically figuring out if text to be made clickable or the URL itself) made more sense to me (for use on the backend of my blog) than either markdown-like approaches or dialogs with two input boxes.

But with my work flow, sometimes it was annoying to have to highlight the link in order to linkify it, and I realized a better UX would be "if I click "linky" and there is no text selected, change every https:// starting URL looking thing into a link." 

This stackoverflow was a good start, but assumed any URL wasn't already in a link - like if you ran it twice (or there were links elsewhere) you'd get <a href="<a href=" crap. 

But the regular expression to do it properly was tough. I got some help and ended up with (with a few tester function to start... and pardon the bad pre code ;-) )

function testLinkify(){

   console.log('starting test'); 

   test(`https://foo.com`, `<a href="https://foo.com"></a>`);

   test(`\nhttps://foo.com`,`\n<a href="https://foo.com"></a>`);

   test(`<a href="https://foo.com"></a>`,`<a href="https://foo.com"></a>`);

   test(`<a href="https://foo.com"></a> https://bar.com`,`<a href="https://foo.com"></a> <a href="https://bar.com"></a>`);

     test(`https://foo.com\nhttps://bar.com <a href="https://baz.com">BAZ</a>`,`<a href="https://foo.com"></a>\n<a href="https://bar.com"></a> <a href="https://baz.com">BAZ</a>`);

}

function test(input,expect){

  const testFunction = linkifyBareHttp;

  const output = testFunction(input);

  console.log (output === expect ? 'PASS':'FAIL');

      console.log(` INPUT: ${input}`);

  if(output !== expect) {

    console.log(`EXPECT: ${expect}`);

    console.log(`OUTPUT: ${output}`)

  }

}

 


function linkifyBareHttp(inputText){

    //URLs starting with http://, https://, or ftp://

    const replacePattern1 = /\b(?<!(\'|\"))(((https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|]))/gim;

    return inputText.replace(replacePattern1, '<a href="$2"></a>');

}

    

testLinkify();


You can play with that at a codepen

(UPDATE: it was funny playing with my admittedly idiosyncratic UI, starting to use it for real. Turns out I had a workflow that expected to be able to click "linky" before typing anything and just get a place holder "<a href>". Also in theory I might hit linky before any textarea has the focus (it uses the rule of "less touched textarea is the 'current' one") so I special cased a few in the offchance I had started focusing yet... anyway...)     

Wednesday, November 16, 2022

container queries for dummies, the complete idiot's guide to moving beyond media queries

So a lot of sites use CSS media queries to switch between mobile and desktop looks, but sometimes you're more concerned about how much room your widget actually has to work with, rather than what the device itself is.

Enter "Container Queries" - fairly wide support but caniuse points out Firefox is lagging (elsewhere claimed its under way)

I found the MDN pages on container-queries a bit confusing, though (especially how it uses "container" both for a class name even though it's part of the "@container" syntax...(

If you had a media query like 

@media (max-width: 600px) {
  /* … */
}

You might try to replace it with 

@container (max-width: 600px){
/* … */
}

The catch is: some ancestor of the content you are actually concerned about resizing has to act as a "containment context"  - some element with the css property 
 container-type: inline-size;
(inline-size is basically width; block-size could be used for height, or size for both. But I think nine times out of ten you're looking for inline-size there)

Here is a minimalist example codepen I made. Ignore the border and padding I put in for reference...  Ignoring the ugly colors and the border and padding for the wrappers... it demonstrates how the context element can have its own width or be constrained by its own parent, etc.

PS Remember the "_____ for Dummies" books? They started with DOS for Dummies... probably it would have been kinder if the title was "for complete newbies" or "with few assumptions about what you already know", but then the alliteration wouldn't have worked...

nothing is forever. even on the internet.


Thinking of "Twitter 2" and my sudden disinclination to rely on their "embed" feature; I wonder how committed they will be to preserving the viability of "legacy" links and embeds... I mean that's part of what let twitter be at the headwaters of other media, it's paradigm was the quotable soundbite, and that quoting could be done EITHER by copy paste (usually with a link) or embed.

And I see that with all kinds of video I've embedded on my blog, especially video (both youtube videos gone private, and then entire services like Vine gone away)

That kind of linkrot makes me sad. Just reminds me of the old school optimism of how "URLs should be forever", like that somehow the (on paper) possibility of endless, perfect duplication meant the brave new "Information Superhighway" of the Internet would be proof against entropy and organizational decay. 

Well, to quote the old Usenet (remember that?) group alt.folklore.computers: "We're all soldiers in the war against entropy."

never rebuild from scratch

Ha, with Twitter's current instability I guess I should quote this tweet from @GergelyOrosz directly:

Scoop: Elon Musk just sent an email to all staff outlining "Twitter 2.0", writing it will"need to be extremely hardcore". Long hours, high intensity.

People need to click "yes" to confirm being part of this by 5pm ET tomorrow, else they get 3 months severance. More details:

It made me think of Joel Spolsky Things You Should Never Do, Part I where he says never "build it up / rewrite it again from scratch" (Netscape was the precursor to Mozilla/Firefox)

Monday, November 14, 2022

get all properties of the event object...

 Playing around with mouse/touch/pointer events, I was a annoyed that Object.keys(e) only returns "isTrusted" when there's lots of good information there... 

In short there's all kinds of "own" (meaning not inherited from a parent object) and"enumerable" aspects messing things up, but short answer is that this stackoverflow suggests 

function getAllPropertyNames (obj) {
    const proto     = Object.getPrototypeOf(obj);
    const inherited = (proto) ? getAllPropertyNames(proto) : [];
    return [...new Set(Object.getOwnPropertyNames(obj).concat(inherited))];
}

and has some other more specific functions as well.

Thursday, November 10, 2022

sticky headers for sub-parts of page in react

 We had a request to have a sticky header for a sub-part of our page, so that as you scrolled down through a long job listing you would always see the "apply" button. (this is on a complex page with job listing cards on the left and the current job being viewed on the right.)

Frankly we overthought; looked at competitor sites, some of them were playing games with changing the heights of parent containers, or letting the big button header scroll up  but then pushing in a smaller mini version of the header. We thought the solution might need Intersection Observer, or maybe switch to "position:fixed" under conditions or some such, but I think we found something pretty cheap and cheerful that does the job. 

The trick is using "useRef" to get the dom element for the (position: relative) header, getting the parent of the "current" ref, using getBoundingClientRect() on the header and the parent to do the appropriate positioning math, and useEffect to set up and tear down the event listner.

End result for the Proof of Concept:

  const headerRef = useRef<HTMLDivElement>(null)

  const adjustHeaderPosition = (headerRef?: HTMLDivElement | null) => {
    if(! headerRef) return;
    const headerRect = headerRef?.getBoundingClientRect();
    const headerHeight = headerRect?.height || 0;
    const parentRect = headerRef?.parentElement?.getBoundingClientRect();
    const parentTop = parentRect?.top || 0;
    const parentHeight = parentRect?.height || 0;
    
    if(parentTop !== undefined && parentTop < 0) {
      let topLocation = parentTop * -1;
      if(topLocation + headerHeight > parentHeight) {
        topLocation = parentHeight - headerHeight;
      }
      headerRef.style.top =`${topLocation}px`;
    } else {
      headerRef.style.top =`0px`;
    }
  }

  useEffect(() => {
    function onScroll() {
      const header = headerRef?.current;
      adjustHeaderPosition(header);
    }
    window.addEventListener("scroll", onScroll);
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  return (
    <JobViewHeaderContainer>
    <JobViewHeaderContainer ref={headerRef}>

(the code is a little careful so as to avoid lint "this could be null!" warnings)

So it makes sure the bottom of the header doesn't go beyond the bottom of parent, and the overall effect is decent.

Thursday, November 3, 2022

click to copy to clipboard widget via javascript

On phones (at least iPhone, probably on Android), copy and pasting is a fiddly operation. (Heck I remember when the first iPhones didn't even have copy and paste, took them a while to get the loupe ui for that right)

One convenient widget I've seen adds a "click-to-copy" widget, like here's one for branch name in git:

I whipped up a version at jsitor.com but it was a little easier to share this codepen....

Here's the JS:

const makeCopyToClipboardWidgets = () => {
  document.querySelectorAll(".click-to-copy").forEach((el, i) => {
    const widget = document.createElement('a');
    widget.innerHTML = '⧉';
    widget.className = 'click-to-copy-widget';
    widget.addEventListener("click", () =>{
      const text = el.tagName === 'A' ? el.href : el.innerText
      navigator.clipboard.writeText(text);
      widget.classList.add('active');
      setTimeout(() => { widget.classList.remove('active');}, 500)
    });
    el.after(widget);
  });
}
document.addEventListener('DOMContentLoaded', makeCopyToClipboardWidgets);

And a little CSS flavoring:

.click-to-copy {
  border: black 1px dashed;
}
.click-to-copy-widget {
  cursor: pointer;
  position: relative;
  top: -4px;
  font-weight: bolder;
  font-size: 1.2em;
}
.click-to-copy-widget.active {
  color: red;
}

So then any DOM object of class "click-to-copy" will get a border and a little icon to click:

(pardon my developer junkbox content)

Some details:
  • I dug around for the Unicode U+29C9, "Two Joined Squares" for the icon
  • I add a little "active" class and the remove it half a second later to provide some visual feedback
  • after I finished the first version, I realized a common usecase is when you want to copy a link - especially useful because it's all to easy to accidentally follow the link as you're trying to copy it on a phone. But sometimes link have a different caption than the content that is worth copying (leaving out "https://", for instance, to make it cleaner). So my code special cases A tags, and grabs the href for them instead of the text...
So, pleased with the outcome. "Vanilla JS" solutions work well for me, because there's a lot on my sites that don't have a node-ish build process... but I admit i still think a bit in terms of jQuery, so that You Might Not Need JQuery page is still a godsend, even though I haven't used jQuery in a while... it was just such a clean syntax for element selection, adding classes, and putting stuff on "ready"...