Show HN: HN Avatars in 357 bytes

701 points by tomxor ↗ HN
Paste the following into the console of any HN page - for annotated avatars on all HN comments. (self contained code)

  for(u of document.querySelectorAll('.hnuser'))for(u.prepend(c=document.createElement('canvas')),x=c.getContext('2d'),c.width=18,c.height=14,s=u.innerText,r=1,i=28+s.length;i--;i<28?r>>>29>X*X/3+Y/2&&x.fillRect(6+2*X,2*Y,2,2)&x.fillRect(6-2*X,2*Y,2,2):r+=s.charCodeAt(i-28,x.fillStyle='#'+(r>>8&0xFFFFFF).toString(16)))r^=r<<13,r^=r>>>17,r^=r<<5,X=i&3,Y=i>>2

535 comments

[ 3.3 ms ] story [ 388 ms ] thread
This was inspired by @frncsdrk's submission from earlier today [0]. The generative concepts used were inspired by and derived from dweets from @KilledByAPixel and @FireFly [1] [2] [3]

The concept is to use HN usernames as the seed into a deterministic avatar generator. This generator is built from the famously simple xorshift32 PRNG, which both provides a random variable for the image generator steps, and "pseudo-hashes" the seed string to provide the initial PRNG state using a non-linear step (adding each codepoint - which is likely not very robust against collisions compared to proper hashing algorithms, but is simple and good enough).

The image generation part is a probability distribution with mirrored pixels... specifically: r>>>29 > X*X/3+Y/2 where the left side is 3 of the upper bits of the PRNG state (providing a random integer 0-7), and the right side is the squared distance from the centre for X + the linear distance from the top for Y. i.e the further from the top centre the pixel is, the less likely it will be filled, but linearly for Y and squared for X.

Un-golfed version:

    for (const u of document.querySelectorAll('.hnuser')) {
        const p=2;
        const c=document.createElement('canvas');
        const x=c.getContext('2d');
            c.width=p*7, c.height=p*7;
            u.parentElement.prepend(c);
        for (let s=u.innerText, r=1, i=28+s.length; i--;) {
            // xorshift32
            r^=r<<13, r^=r>>>17, r^=r<<5;
            const X=i&3, Y=i>>2;
            if (i >= 28) {
                // seed state
                r+=s.charCodeAt(i-28);
                x.fillStyle='#'+(r>>8&0xFFFFFF)
                .toString(16).padStart(0, 6);
            } else {
                // draw pixel
                if (r>>>29 > X*X/3+Y/2)
                x.fillRect(p*3+p*X, p*Y, p, p),
                x.fillRect(p*3-p*X, p*Y, p, p);
            }
        }
    }
Was fun to play with, but also surprisingly helpful in following discussions.

It's worth mentioning these were heavily inspired by three particular dweets by @KilledByAPixel and @FireFly:

[1] https://www.dwitter.net/d/3078

[2] https://www.dwitter.net/d/19786

[3] https://www.dwitter.net/d/23326

[0] https://news.ycombinator.com/item?id=30660316

I've been following Frank Force (KilledByAPixel) for quite some time now, he's on the forefront of generative design via HTML canvas.
Yeah! he's done a deep dive into it more recently. Attempting to follow his twitter thread at the moment is like trying to consume a fire-hydrant of generative art :D hard to keep up, but looks amazing.
There's actually a slight difference between the golfed and un-golfed version: the un-golfed adds the avatar to the beginning of the line with the username, whereas the golfed version adds it right next to the username. I actually find it to be more visually appealing at the beginning of the line, so I'm sticking with that in my Greasemonkey script.

To anyone interested, I've added links to how the un-golfed version looks with Dark Reader enabled[1] and how it looks without Dark Reader[2]. To clarify, I've set my zoom level for Hacker News to 150% as I find the default to not be conducive to reading.

---

[1]: https://i.imgur.com/hw9pn0l.png

[2]: https://i.imgur.com/z3jV2NZ.png

Yup, i noticed an opportunity to make it smaller while golfing.

There is one advantage of the golfed version, which is that it will also inject avatars wherever it finds the .hnuser class, including on other pages.

I've never heard of dweets/dwitter before; thanks for sharing. For anyone else:

Dwitter.net is a challenge to see what awesomeness you can create when limited to only 140 characters of javascript and a canvas. Give it a go!

https://www.dwitter.net/

Your minified version lacks the .padStart(0, 6) you have in this comment, without which colour selection fails in just under 1⁄16 of cases, yielding black. (Or in just under 1⁄256 of cases, a semi-transparent colour!)
Thanks, I know it's absent (it's golfed not minified), which means I manually manipulated it to make as small as possible which sometimes means making "concessions" where things are not ideally preserved - But in retrospect you're probably right, I was being too aggressive and should have either left the padding in or replaced it with some guarantee of formatting length length like "&FFF|0x111"
further ungolfed:

  for (const huser of document.querySelectorAll(".hnuser")) {
    const s = huser.innerText;
    const p = 2;
    const canvas = document.createElement("canvas");
    const context = canvas.getContext("2d");

    canvas.width = p * 7;
    canvas.height = p * 7;

    huser.parentElement.prepend(canvas);

    {
      const xorshift32 = (n) => {
        n ^= n << 13;
        n ^= n >>> 17;
        n ^= n << 5;
        return n;
      };

      const seedSteps = 28;

      let seed = 1;
      for (let i = seedSteps + s.length; i >= seedSteps; i--) {
        seed = xorshift32(seed);
        seed += s.charCodeAt(i - seedSteps);
      }

      context.fillStyle =
        "#" + ((seed >> 8) & 0xffffff).toString(16).padStart(0, 6);

      for (let i = seedSteps - 1; i > 0; i--) {
        // continue the seed
        seed = xorshift32(seed);

        const X = i & 3;
        const Y = i >> 2;

        if (seed >>> (seedSteps + 1) > (X * X) / 3 + Y / 2) {
          context.fillRect(p * 3 + p * X, p * Y, p, p);
          context.fillRect(p * 3 - p * X, p * Y, p, p);
        }
      }
    }
  }
You're off by one in the initial seed step. Change

for (let i = seedSteps + s.length; i >= seedSteps; i--) to for (let i = seedSteps + s.length - 1; i >= seedSteps; i--)

The original is not using the for loop in the usual way so i starts off one smaller than expected.

It would be neat if the code also applied to the username in the top right so I didn't have to make a spammy comment to see what it gives me. ;)
Just F12 and manually edit some username, after that re-paste the code and you will see the avatar.
The other way is to just navigate to your own profile page and apply the script there.
Just replace the '.hnuser' bit with '.hnuser, #me'
Wow this is really great! Noob question, but let's say I wanted to permanently load this JS in my browser? How would I achieve that? Grease monkey? Extension? Thanks :)
Should be really easy with grease monkey. I simply pasted the snippet into a new script and enabled it for this site. Works perfectly out of the box.
Violentmonkey is a nice open source alternative to Greasemonkey.
Your wording implies Greasemonkey is not open source. I don’t know if that was your intention or not, but Greasemonkey is open source.
They're probably confusing it with Tampermonkey, which isn't open source.
Or ask pg to save it on the server side.
Why not create your own web extension? It will take just a few lines of code, you can look at this example for reference[0] (also on Github[1]).

[0]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/Web...

[1]: https://github.com/mdn/webextensions-examples/tree/master/bo...

I don't know about you, but I really don't want an extension for every small change one might want to apply to one specific site.
That's fair how about Tampermonkey -- I use it occasionally to apply js/css/html fixups to sites, e.g. Add browser titles to sites for better bookmarks, re-organize/add site html, etc. I did use it for removing content but ublock origin is much better imo for most cases.
huh. is this something we can control?
In a way, yes. But from the way you phrased it, it sounds like you believe it is a HN feature, which it isn't. It's javascript running in your browser that dynamically generates the avatar via canvas2d and adds it wherever it finds the .hnuser class.
I wonder how easily you could have slipped in an obfuscated cookie stealer
UTF-encode the maliscious snippet and disguise it as the regex pattern haha
Doesn't UTF-8 just give you the exact same letters, since it's already ASCII?
I think it was meant to be UTF-8 escape codes, as in \u0000 etc.
Ahh that makes sense, thanks.
Pedantry: These are really Unicode escape codes not UTF-8 escape codes, since they're codes for Unicode code points. You can write invalid UTF-16 with these escapes (because unlike UTF-8 the UTF-16 encoding actually borrows Unicode code points that are not scalar values, called the "surrogates") but I don't know what the standard says should happen if you do that.
HN sets one cookie and that is "httpOnly" and "secure", so it can't be read from JavaScript. You can test this yourself by typing document.cookie into your console, it should return an empty string.
Given that it takes about 20seconds to read through the snippet, not easily ;-)
(comment deleted)
I'm trying to do this in Firefox 98.0 and I'm not able to. I see this in the console:

> Scam Warning: Take care when pasting things you don’t understand. This could allow attackers to steal your identity or take control of your computer. Please type ‘allow pasting’ below (no need to press enter) to allow pasting.

Oh now that I'm reading it carefully, I see there's a way to enable it!

(comment deleted)
This is interesting :D

Setting it as

  javascript:<code>;void(0); 
can make it useful as a boomarklet :)
Point of information: the ;void(0); isn’t necessary here because the code ends in a for loop, which produces undefined.

(For anyone who doesn’t know about the meaning of void in JavaScript: it’s a unary operator that ignores its operand and produces undefined. What is commonly spelled `void(0)` is briefer as `void 0`. Why don’t people just write `undefined`? For some, because it’s shorter, but mostly for historical reasons: `undefined` is a global property rather than a keyword, and until ES5 (a decade ago), that global property was writeable, so you couldn’t rely on its value because people might do crazy things. And if you’re wondering why a javascript: URL needs to produce exactly the value undefined: if it doesn’t, the browser will convert the value to a string and replace the page with that, loaded as HTML, as demonstrated by this (paste it in your address bar, you can’t click on data: URLs directly for security reasons):

  data:text/html;charset=utf-8,<a href="javascript:'<h1>Here is a page.'">α</a> <a href="javascript:({toString(){return '<h1>See, stringification!'}})">β</a> <a href="javascript:/*even*/null/*gets stringified*/">γ</a> <a href="javascript:undefined">δ</a>
.)
It wasn't actually working for me; it was returning 0 actually. But yeah, this is a good point as well. :D
Huh, turns out my understanding of how it worked was wrong: in things like for loops, it takes the last value encountered. I should have anticipated that, really, given that “last expression” isn’t generally a thing in JavaScript anyway (it’s statement-oriented, not expression-oriented), so javascript: URIs (and I guess on* event handler attributes too, I know they’re a bit weird too) were clearly doing something clever and not-usual-JavaScript. I should delve into specs until I can find it; it seems like it’s at least mildly expression-oriented in character.

I retract the first paragraph of my earlier comment with an apology.

I just put onion2k's version as a script in violent monkey, set to run only on NH.
For better rendering on high-DPI screens and (more subjectively) screens of people that have zoomed in because HN’s text sizes are unreasonably small, add c.style.imageRendering='pixelated'.

I was going to say that the insertion of these 24 bytes allows you to save ten bytes elsewhere by halving the size of the image (making features one pixel in size, rather than two), but then you’d also have to set the doubled image dimensions, and that’d cost more than ten bytes.

(Me, I use HN at 120% on top of a native scaling factor of 150%, which is currently implemented as downsampling of 200% rendering because Firefox hasn’t got fractional scaling right yet under Wayland, and widget.wayland.fractional_buffer_scale has some fairly debilitating side-effects on popup windows including non-rendering and crashes, which is a pity because it improves rendering so very much.)

It's a good point, I did consider using that feature but the problem is it's not very cross browser friendly, so I stuck with simpler DIY pixel scaling.

In fairness I think there is basically two different values for chrome and firefox, although I'm not sure about safari and cannot test it...

Firefox shipped the value "pixelated" five months ago, so you don’t need to worry about a "crisp-edges" fallback any more. Safari, back in 2016. https://caniuse.com/css-crisp-edges

This is valuable for high-DPI and zoomed pages, to avoid bilinear or similar scaling; the matter of halving the canvas size was a red herring.

Ah, good to know it's finally normalised.
(comment deleted)
(comment deleted)
Ive been a dev for over 2 decades but I still cant grok bitwise operators - anyone have a reference that may help with that?
I've found the python docs[0] to be quite helpful for this topic.

[0]: https://wiki.python.org/moin/BitwiseOperators

Thanks, but its still a bit confusing for me. Im looking for a really dumbed down kind of approach: input, operator, visual explanation of what its doing, output. Im a really bad learner, and didnt study computer science or anything. Ive never had a reason to touch binary, so find these operators unintuitive.
If you don't know about the binary representation, you won't be able to learn binary operators. If you do, they're fairly straightforward. I'd suggest learning about binary first.
I think before you can understand binary operators, you’ll need to understand binary itself and how to convert it to and from decimal. This crash course computer science video may help: https://youtu.be/1GSjbWt0c9M

In fact the whole course might be useful!

I understand the conversion and representation - i guess I just dont understand the intent behind the usage of these operators.

Without stepping through each line in a debugger I literally have no idea what the ungolfed version of the main post is doing, or why its doing it - and this seems to be typical of my experience with bitwise operators.

But I will definitely have a look at the references you have given. Hopefully that will give me a more intuitive understanding. Thanks.

There's many reasons for bitwise operators to exist.

For instance it's extremely easy, when looking at the binary representation of a number to check the correct end of the byte and determine if a number is odd or even.

You can use bitwise operators to bit shift and check if a value is over a certain amount but seeing if there is any value after a bit shift.

there's tonnes of real world applications:

* Bit fields (flags)

As they're the most efficient way of representing something whose state is defined by several "yes or no" properties. ACLs are a good example; if you have let's say 4 discrete permissions (read, write, execute, change policy), it's better to store this in 1 byte rather than waste 4. These can be mapped to enumeration types in many languages for added convenience.

* Communication over ports/sockets

Always involves checksums, parity, stop bits, flow control algorithms, and so on, which usually depend on the logic values of individual bytes as opposed to numeric values, since the medium may only be capable of transmitting one bit at a time.

* Compression, Encryption

Both of these are heavily dependent on bitwise algorithms. Look at the deflate algorithm for an example - everything is in bits, not bytes.

* Finite State Machines I'm speaking primarily of the kind embedded in some piece of hardware, although they can be found in software too. These are combinatorial in nature - they might literally be getting "compiled" down to a bunch of logic gates, so they have to be expressed as AND, OR, NOT, etc.

* Graphics

There's hardly enough space here to get into every area where these operators are used in graphics programming. XOR (or ^) is particularly interesting here because applying the same input a second time will undo the first. Older GUIs used to rely on this for selection highlighting and other overlays, in order to eliminate the need for costly redraws. They're still useful in slow graphics protocols (i.e. remote desktop).

the wiki is a visual representation. The representation is 0s and 1s.

The use case for bitwise operations is to control the value of 1 bit.

Consider if you have a 0000 1000 value. It's mapped to hardware output so the 4th LED is on. Now you want to turn the 5th on, and the 4th off. You would use a left shift so the output becomes 0001 0000.

That's all there is to it. Bitwise operators operate on bits, not bytes. Because computers operate on bytes, and you can't address a bit - you have to modify bytes to accomplish your goal.

(comment deleted)
> Bitwise operators operate on bits, not bytes. Because computers operate on bytes, and you can't address a bit - you have to modify bytes to accomplish your goal.

I think this may be the fundamental intent I was missing. How do people wrangle these things in their head! Is it ever intuitive, or are even the proficient externally visualising this stuff in some way?

Try thinking about the equivalent in decimal:

A left shift in binary adds a 0 at the end which multiplies by 2 (binary = 2)

In decimal, the equivalent, adding a 0 at the end, multiplies by ten (decimal = ten)

You'll just have to use it in actual programs to really understand it. You can't properly understand `if` and `for` either just by reading some doc page about it, you can't become a guitarist just by reading about technique, you don't become a carpenter just about reading about it, etc. So don't feel bad if you're confused by just reading that page.
I struggled with this as well. Now I'm a bit rusty as I haven't used it for a while, but what really helped me was to start thinking about binary numbers more as strings. It also really helps me to visualize binary operations in binary space, not hex or decimal. That's still very confusing to me.

For example left shift by 1 on this 8 bit number (decimal is 32)

00100000

you just shift all the numbers to the left

01000000

--------

There's some wrapping behavior of course. Like what happens if you left shift 3 here? If it's an unsigned 8 bit number the 1 would teleport to the right side.

00000001

--------

the logical operators work like this (read downwards)

00010000 AND

00010000 =

00010000

--------

00001000 AND

00010000 =

00000000

--------

00001000 OR

00010000 =

00011000

With this in mind, I found it to be a good exercise to implement common bitwise operators that work on fixed length strings with some tests against the real binary operators in your favorite language.

> It also really helps me to visualize binary operations in binary space, not hex or decimal. That's still very confusing to me.

Yeah, I think thats a major part of the problem for me. When im looking at these code snippets (as per ungolfed version of the post in a comment below) applying the operators to integers i just cant understand what it is they are trying to achieve, so I am unable to grok the purpose/intent. Even with the explanations given here im not sure why its doing what its doing - and I dont seem to encounter that issue outside of bitwise operators.

(comment deleted)
> There's some wrapping behavior of course. [...] the 1 would teleport to the right side.

Not really. Shifting by definition discards 1 bits once they pass either "edge" of the value. The wrapping you describe only happens with rotation operations. In the languages I know they don't have fancy short hand syntax like shifting does.

<< means shift left (moves the bits to the left side)

1 << 1 = 2 because (0001 << 1 = 0010 = 2)

3 << 1 = 6 because (0011 << 1 = 0110 = 6)

>> works in the same way

2 >> 1 = 1 because (0010 >> 1 = 0001)

The other operators are exactly the same you'd find in electronics (and boolean algebra):

~ is the NOT operator

& is the AND operator

^ is the XOR operator

Sorry, I don't have a visual representation for this (I haven't looked for a link) but I would strongly suggest to write it down on paper and do some small exercises (checking with the result then on your favorite programming language).

On a day to day basis, you'd likely not use these operators at all, but as soon as you start decoding protocols or working on a bit-level, those are super useful!

I would suggest trying to decode a bitstream protocol to really grasp the concepts: you'd use masks and bitwise operators a lot :)

---

Edit: some useful links I've found: https://www.interviewcake.com/concept/java/bit-shift

It helps if you look into bits and binary arithmetic; I learned it from the appendix of my school book Structured Computer Organization. Binary and binary arithmetic is probably the best place to start.
Reading didn't do the trick for me. It clicked when I started writing code to accomplish tasks, like assignments or coding puzzles, that required bitwise operations.

I can't find any of my undergrad coursework online but here's an alternative that looks like a nice intro https://web.stanford.edu/class/archive/cs/cs107/cs107.1194/l...

(comment deleted)
This is fun.

One suggestion.. rather than creating a canvas for each user using a querySelectorAll and a loop, I'd use an IntersectionObserver and only create the canvases as they scroll into view. That way the user's device won't need to create hundreds of elements when the code runs.

    let observer = new IntersectionObserver(
    (entries) => {
        entries.forEach((entry, i) => {
        if (entry.isIntersecting) {
            const p = 2;
            const c = document.createElement('canvas');
            const x = c.getContext('2d');
            c.width = 18;
            c.height = 14;
            const s = entry.target.innerText;
            const r = 1;

            if (s) {
            for (
                let s = entry.target.innerText, r = 1, i = 28 + s.length;
                i--;

            ) {
                // xorshift32
                (r ^= r << 13), (r ^= r >>> 17), (r ^= r << 5);
                const X = i & 3,
                Y = i >> 2;
                if (i >= 28) {
                // seed state
                r += s.charCodeAt(i - 28);
                x.fillStyle =
                    '#' + ((r >> 8) & 0xffffff).toString(16).padStart(0, 6);
                } else {
                // draw pixel
                if (r >>> 29 > (X * X) / 3 + Y / 2)
                    x.fillRect(p * 3 + p * X, p * Y, p, p),
                    x.fillRect(p * 3 - p * X, p * Y, p, p);
                }
            }
            }

            entry.target.prepend(c);
        } else {
            if (entry.target.firstChild.tagName === 'CANVAS')
            entry.target.firstChild.remove();
        }
        });
    },
    { rootMargin: '0px 0px 0px 0px' }
    );

    document.querySelectorAll('.hnuser').forEach((user) => {
    observer.observe(user);
    });
(comment deleted)
Does the canvas need to be created and removed as it comes in and out of view? Using requestAnimationFrame() might also further improve responsiveness.
Does the canvas need to be created and removed as it comes in and out of view?

Not really. You could just create them and not bother removing them. You'd need to check if the user already had an avatar if you did that though, or you'd end up with lots of repeated avatars when you scroll up and down the page.

Or just generate them on page load and call it a day. No bugs, no complexity, no edge cases.

Could it be faster? Sure, but should it be faster? Not really, IMO, the avatars load very quick, even on a big page.

Speaking performance/optimisations, interesting choice to destroy canvas when it leaves viewport and recreate it upon each re-entry.

Personally I'd leave it created and stop observing the element instead, à la

    /* … */ if (entry.isIntersecting) { observer.unobserve(entry.target); /* … */ }
Do you think (or know) that swapping observed node for canvas (potentially producing many canvases) is more expensive than keeping all observers and having smallest possible amount of canvases? (Maybe I'm biased towards saving CPU but saving RAM is better after all?)
That's a really good idea. Definitely something to measure, but I suspect you're right.
If it turns out there’s some upper bound where caching canvases in memory becomes a problem (eg large threads become not paginated), it’s pretty trivial to build a cache around [Weak]Map and get the ~best of both worlds.
This is great, thanks. A small fix I made to stop console warnings was to change:

  x.fillStyle = '#' + ((r >> 8) & 0xffffff).toString(16).padStart(0, 6);
to:

  x.fillStyle = '#' + ((r >> 8) & 0xffffff).toString(16).padStart(6, '0');
as the pad target length is the first parameter of padStart.
Thanks, Interesting idea. I'm a little sceptical of the performance improvement, but I suppose that depends what we mean by performance.

Your strategy essentially trades a one time computation and DOM mutation with a continuous but lighter one with some added overhead. In this case I suspect the total power performance is worse over time; _however_ latency and UX should be _better_, since it removes the relationship between total comments on the page and the time to render avatars, which is currently about 100ms for this page on my machine.

I'm definitely biased towards preferring a one time change and making it as efficient as possible, but when the input has a high enough ceiling I can see how your strategy would make more sense - I'm not quite sure where I stand on HN threads - but thankfully this is just a user script so we can make our own choices :)

I wonder how `<canvas>` would compare to `<img src="data:image/svg..."/>` instead
Yes! it did cross my mind, I was just more familiar with canvas API so I cracked on with it... but I suppose encoding a simple format like bitmap would be trivial, almost like being able to access the canvas pixel buffer but with a lower overhead.

This thread is a good perf test, I might give it a quick go.

Also as others mentions the pixelation style thing is now finally fully cross browser supported, which should make it easier to achieve nearest neighbour upscaling for url encoding.

[edit]

I underestimated how involved image format headers are!

A nice improvement! An unfortunate side-effect of it is that it introduces some jitter during macOS's smooth scrolling in Firefox at time of first render, though.
Awesome! I did not know about IntersectionObserver. Will def add this to my toolbox! Thanks!
A better optimization would be adding appropriately sized SVG <use> tags to comments before first paint (to prevent re-flowing the layout) and lazily generate avatars to a SVG sprite sheet. No canvas required.
An unkown person asks you to inject code in your browser.

Sorry folks, but this should be a red flag for all of you. I guess you all know enough about obfuscation to know that what you think the code does is not always what the code does.

"But it has upvotes!" For all you know, the script could upvote this post.

Even when obfuscated, the code looked safe to me. (But I did wonder, and thankfully, HN has set httpOnly on the cookies.)
It is very short and quite easy to see that it is safe, even if you do not fully grok what is going to happen. If you don't, then by all means do not run it.
It's running in a sandboxed environment(the web browser). The worst-case scenario is it gaining access to my HN account, and I don't really care that much about it.

It's not going to install a bootsector virus on my machine, or ransomware-encrypt all the data on my NAS. So the risk of anything important happening is small, and the same risk as visiting any website on the internet.

I've got a pooping person hm
I didn't see it as that until you mentioned it.
You got a piranha plant.
...and I have... um... an octopus carrying a block of wood?
(comment deleted)
it's a single eye, squinting, either in concentration or distrust
I think you have a storm trooper.
Yeah I thought it was a tribal ritual man sort of thing... but now that you mention it...
I seem to be the Github Octocat
What would be the Python, server-side version of it?
Hah! I got a really cool little icon that looks almost like the cassette tape pirate logo of Piratbyrån back in the day. I have old t-shirts with that logo.

MAKE. THIS. PERMANENT!

On the functional side, this really helps you see who is who in a long threaded discussion. Your eye is much quicker at following the little color icons than their names.

I'm gonna go ahead and vote no on this.
But your avatar looks like a Ninja Turtle with shades on. You should be enjoying this. :)
Likely still traumatized by existence of new reddit UI. Old appears to be better.
I just find pfps visually distracting and enjoy HNs stance as one of the few remaining sites keeping an old school look. I feel like it would affect the tone of the site and negatively affect the quality of the conversation.
Wow, I thought it was a green camper van!
It should be fairly straightforward to turn this into a browser addon, give users a choice and offload the people behind HN.
Or even easier to make a userscript: https://openuserjs.org/
What is a user script?
A JavaScript script injected into a website by the user (as opposed to the website).

  // ==UserScript==
  // @name        HN Avatars in 357 bytes
  // @description Annotated avatars on all HN comments.
  // @author      tomxor (https://news.ycombinator.com/user?id=tomxor)
  // @namespace   https://news.ycombinator.com/item?id=30668137
  // @include     https://news.ycombinator.com/item*
  // @include     https://news.ycombinator.com/user*
  // @grant       none
  // @version     1.0.0
  // ==/UserScript==

  [js goes here]
>MAKE. THIS. PERMANENT!

I second this. (Also I wanted to see what my avatar looks like, sry)

How dare you! Edit: ooooh, mine's a beetle!
Truly unacceptable. (I look like an alien head!)
But Space Invaders is a classic!
Only commenting so I can see my avatar...
How dare you, you cyan flower vase on wheels :) I'm a spiky small yellow watchtower, yay.
And me? A green butterfly?
Yup. Or a trophy/ a goblet maybe? Not sure if green stands for 1st, 2nd or 3rd price though, up to you to decide :)
hi everyone :) I am a happy jumping frog :)
Commenting to see mine. Edit: Excellent! It's a blob.
Let's see what mine is Edit: I love it! A little bug :)
Looks like Shrek's forehead. Posting for my own avatar reveal...
This is fun, and can easily be added as a userscript. I might look to do that a little later.
// ==UserScript== // @name HN icons // @namespace https://news.ycombinator.com/* // @version 0.1 // @description try to take over the world! // @author some people on HN // @match https://news.ycombinator.com/* // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant none // ==/UserScript==

(function() { // Initial concept is from: https://news.ycombinator.com/item?id=30668137 (tomxor) // The script we are using is from: https://news.ycombinator.com/item?id=30670079 (onion2k) 'use strict'; let observer = new IntersectionObserver( (entries) => { entries.forEach((entry, i) => { if (entry.isIntersecting) { const p = 2; const c = document.createElement('canvas'); const x = c.getContext('2d'); c.width = 18; c.height = 14; const s = entry.target.innerText; const r = 1;

            if (s) {
            for (
                let s = entry.target.innerText, r = 1, i = 28 + s.length;
                i--;

            ) {
                // xorshift32
                (r ^= r << 13), (r ^= r >>> 17), (r ^= r << 5);
                const X = i & 3,
                Y = i >> 2;
                if (i >= 28) {
                // seed state
                r += s.charCodeAt(i - 28);
                x.fillStyle =
                    '#' + ((r >> 8) & 0xffffff).toString(16).padStart(0, 6);
                } else {
                // draw pixel
                if (r >>> 29 > (X * X) / 3 + Y / 2)
                    x.fillRect(p * 3 + p * X, p * Y, p, p),
                    x.fillRect(p * 3 - p * X, p * Y, p, p);
                }
            }
            }

            entry.target.prepend(c);
        } else {
            if (entry.target.firstChild.tagName === 'CANVAS')
            entry.target.firstChild.remove();
        }
        });
    },
    { rootMargin: '0px 0px 0px 0px' }
    );

    document.querySelectorAll('.hnuser').forEach((user) => {
    observer.observe(user);
    });
})();
This should be pretty easy to make into a userscript if browsers still support that. I haven't checked on that in half a decade or so

also i wanted to see mine too

> Hah! I got a really cool little icon that looks almost like the cassette tape pirate logo of Piratbyrån back in the day.

I think it's a skeleton holding his arms up!

edit: Looks like I'm some green bug-monster

I do feel like HN needs avatars of some kind... just don't know if they should be customizable or fixed (like this one).
Definitely not customizable, that's chaos.

Unique, fixed little color doodads make it easier to see who's who in a thread.

With how pretty much every platform has "avatars", that the user can customize, I think it would be fun just getting one assigned to you.

Edit: Huh, I got an alien house. Reminds me of some game.

I'm for the fixed avatars. We are all born with certain unchangeable aspects.
Hey man, we were just discussing avatars, not trying to create an existential crisis.
Perhaps I'm a little fighter jet from an 8-bit arcade?
i love it, its very cute, reminds me of the old MSX
Or a guy with a tophat! Dig it.
Yours reminds me of the Wild Hunt from The Witcher, at least the crown
Trying mine.
This is awesome and an excellent way to start my day, thank you !
Ooooh what will I get

Edit: hmmm, kinda looks like a Tori gate.

Looks way better if you add style="image-rendering: pixelated;" to the canvas. Crisp edges on every display.
Yup, definitely second that. With that change I can definitely imagine this as a user option in the settings. Reading is so much easier.

(also wanted to see my avatar)

It does! Here's a bookmarklet with that added (generated here: https://caiorss.github.io/bookmarklet-maker/)

javascript:(function()%7Bfor(u%20of%20document.querySelectorAll('.hnuser'))for(u.prepend(c%3Ddocument.createElement('canvas'))%2Cx%3Dc.getContext('2d')%2Cc.width%3D18%2Cc.height%3D14%2Cc.style.imageRendering%3D%22pixelated%22%2Cs%3Du.innerText%2Cr%3D1%2Ci%3D28%2Bs.length%3Bi--%3Bi%3C28%3Fr%3E%3E%3E29%3EXX%2F3%2BY%2F2%26%26x.fillRect(6%2B2X%2C2Y%2C2%2C2)%26x.fillRect(6-2X%2C2*Y%2C2%2C2)%3Ar%2B%3Ds.charCodeAt(i-28%2Cx.fillStyle%3D'%23'%2B(r%3E%3E8%260xFFFFFF).toString(16)))r%5E%3Dr%3C%3C13%2Cr%5E%3Dr%3E%3E%3E17%2Cr%5E%3Dr%3C%3C5%2CX%3Di%263%2CY%3Di%3E%3E2%7D)()%3B

That's a neat little snippet. Thank you for sharing it. :)
This is cool. Should totally be part of official HN js
Should be an unlockable feature after 100 karma through a checkbox in your profile
Fun showcase, but truthfully I would not want this to become a permanent feature. A browser plugin would work.