8 ms·
Show HN: A Stargate in 140 chars of JavaScript
- tomxor 6y agoz = n => n--&&C(T/2+S*T/R+t*9)*C(T/4-z(n)*2)-r/6 for (c.width=192,R=48,i=8064; i--;) x.fillRect( S+96, T+54, C(Math.atan2(S=i%97-R,T=i/97-R,r=(S*S+T*T)**.5)*9)*20-r&44&& r>36?r<42?T/R:S/R:1, r>36?r<R:z(3)/5 )
- tomxor 6y agoI'll try to give a rough overview of what's going on here. The function z produces the puddle (ahem - "event horizon") through a technique I accidentally found from "orthogonal" raymarching. The accident being the value is supposed to be the distance of an incrementing ray for an SDF (signed distance function), but instead the distance is completely replaced on each recursion resulting in the reflected fluid appearance. I think this is due to the ray distance oscillating backwards and forwards in a way that is similar enough to neighboring rays to not appear noisy while giving the appearance of reflection. The exact functions used for the SDF are not so important so long as it incorporates itself. fillRect is doing pretty much everything else, by painting each pixel. To achieve shading without fillStyle, the width and height arguments are varied between 0 and 1 to give grey scale shading due to subpixel anti-aliasing. Speculative edges are also achieved with negative values (0 to -1) which creates white gaps and fill adjacent pixels instead. The chevrons are sampled from a radial fractal a*20-r&44 where 'a' is the angle of the current position wrapped in some periodic function (cosine), and 'r' is the radius. The bitwise AND operator is the key in generating these types of patterns (although OR and XOR also work very well producing other types of patterns), the constants are found through - a lot - of trial and error. In this case the result of the fractal is used as a "truthy" condition (i.e non zero) to toggle between the shaded sides (X/48 or Y/48) and 1 (the dark bits). Some other hacky details: S and T are the X and Y pixel positions, these names are used so that they can be referenced before they have been assigned because these names have already been assigned unused functions by dwitter. This saves some characters by allowing me to define X and Y inside of atan2 while also passing them as arguments, at the cost of the fillRect X Y position being invalid for the first pixel and from the previous iteration on the rest. I'm finding a recurring property of dweets that end up packing a lot in - is sharing significant portions of code for different purposes, in this case it's fairly straight forward but still a big win in terms of characters: S, T and r are only defined once and used for both the puddle and the ring. This type of size optimization always gives a lot more road than syntactic wrangling, which while important are only finishing touches. However they cannot be forced, ideas with enough shared code can only be found. Feel free to ask me anything else.
- kortex 6y agoMind-blowing work! Is there any kind of tools for "reverse engineering" or otherwise unrolling/prettifying dweets so I can study them better? It's really cool but I barely know where to begin.
- tomxor 6y agoThanks! I've never found prettifiers to help much. Often there isn't much in the way of indentation or newlines for a formatter to be able to objectively insert. I mainly rely on the following while both making my own dweets and unpacking other dweets (which can be just as much fun btw): 1. Use a text editor: Dweets can get very dense and non linear, syntax highlighting can help a lot when deconstructing a dweet (essentially manually prettifying). I also find variable highlighting invaluable for understanding a dweet (i.e where you select one and all of the others are clearly highlighted). Often the code will be arranged in confusing ways just to squeeze extra characters out, e.g putting things in the incrementor position of a for loop 'for(i=8;i--;x.fillRect(blah))', often you will want to rearrange it to make it more legible while you are understanding the code. 2. Use Dwitter: When making new dweets or figuring out the behavior of existing dweets. The code runs as soon as you lift each finger, this creates a feedback loop between your brain and the computer which is faster than any other interactive computing environment i've seen. It seems absurdly simple but it's very powerful and you will learn a lot. JS things.. all of those niggling assumptions that you don't have time or inclination to investigate when writing "proper" code will suddenly be easy and necessary and interesting to figure out. Math things.. I usually suck at math, but when you can play with the variables and functions and watch patterns instantly change on the screen math suddenly feels like the most intuitive thing in the world. 3. Your brain: The lovely thing about code this size is it can all fit in your head with ease... After marinading in your head for long enough eventually the code falls away and the concepts remain, at which point you can think about it, run it, and manipulate it with ease. [edit] Quite a number of dweets are packed using the unicode escape trick these days to get 194 effective characters (including this one). To quickly get the unpacked code just replace 'eval' with 'throw' and it will be printed bellow the dweet. Finally there is a small but active discord group where you might find people willing to help with golfing (or ungolfing) and figuring things out etc: https://discord.gg/emHe6cP https://discord.gg/emHe6cP
- visarga 6y agoI can shrink it to 50 chars: window.location='https://www.dwitter.net/d/20584'; https://www.dwitter.net/d/20584';
- rendall 6y agoI for one think that's funny and clever
- sq2 6y agoLegend
- tomxor 6y agoA few commenters have pointed out this is not 100% vanilla. I've described how minimal dwitter's shim is, but I'd like to demonstrate it: Paste the following into any browser console and it will run all by itself: Unpacked = 303 chars (+109): document.body.innerHTML='<canvas id=c>';x=c.getContext('2d');t=0;setInterval(_=>{for(X=Y=C=Math.cos,t+=1/60,c.width=192,R=48,i=8064;i--;x.fillRect(X+96,Y+54,C(Math.atan2(X=i%97-R,Y=i/97-R,r=(X*X+Y*Y)**.5)*9)*20-r&44&&r>36?r<42?Y/R:X/R:1,r>36?r<R:z(3)/5))z=n=>n--&&C(Y/2+X*Y/R+t*9)*C(Y/4-z(n)*2)-r/6},16)
- lifthrasiir 6y agoOr as a standalone HTML file, 272 bytes (EDIT: was 284 before, I did try a bit more) suffice: <body onload="t=setInterval('with(Math)for(t+=.144,c.width=192,R=48,i=8064;i--;c.getContext`2d`.fillRect(X+96,Y+54,cos(atan2(X,Y)*9)*20-r&44&&r>36?r<42?Y/R:X/R:1,r>36?r<R:z(3)/5))r=hypot(X=i%97-R,Y=i/97-R),z=n=>n--&&cos(Y/2+X*Y/R+t)*cos(Y/4-z(n)*2)-r/6',16)"><canvas id=c>
- lxe 6y agoWhy does escape'ing and un-escape'ing that string result in code?
- deleted 6y ago[deleted]
- tomxor 6y agoThere is a critical bit of regex in between. In short this technique unpacks two ASCII characters from each unicode point of the input string. Any ASCII safe code can be packed with this dweet: https://www.dwitter.net/d/11852 https://www.dwitter.net/d/11852 The escape function [0] converts unicode values > 0xFF into the string format '%uxxxx`, additionally it will produce two sequences if the unicode point is high enough to require two UTF-16 surrogate pairs. e.g escape() ing the first character '𩡯' results in '%uD866%uDC6F'. The regex throws away the 'uD8' parts of each sequence as we aren't exploiting the full capacity of the unicode space, just the lower 255 values of each surrogate pair. which results in '%66%6F'. Finally this is unescaped which converts these into the separate ASCII chars that were originally packed with dweet 11852 (don't ask me how it packs it I haven't looked yet.) In all this means it's possible to fit 194 evaluable ASCII chars into a dweet (taking into consideration overhead of the unpacking code). There are also more esoteric methods of fitting more characters or data using unicode that are not generally evaluable, e.g image data. All of these techniques are using more than 140 bytes, but dwitter has a character limit not byte limit. But nothing is free, you must fit any decoders into the same dweet so there is always a balance. [0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/escape https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
- jugg1es 6y agoyou are a text master. we should call you Master Text.
- scintill76 6y agoI was wondering how (escape`str`) is valid JS to invoke the escape function. Now I know it's called "tagged template literal."
- knodi123 6y agohow can a person actually run this? If I paste it into my JS console, I just see Uncaught EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self' 'unsafe-inline' https://www.google.com/recaptcha/ https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://cdnjs.cloudflare.com/ https://cdnjs.cloudflare.com/".
- tomxor 6y agoDwitter sets up a minimal shim with a canvas `c` and 2d context `x`, and some shortcuts to math functions sin `S`, cos `C`, tan `T`, and then runs a 60 FPS loop with time `t`. The code is run in this loop with those variables in non-strict mode to allow various hacky things e.g `with()` that are invaluable for code golfing. The eval escape unescape trick extracts two chars in the ASCII space from each UTF16 character. Here's the encoder: https://www.dwitter.net/d/11852 https://www.dwitter.net/d/11852 You can see the content by removing the eval and just outputting the string... alternatively replace 'eval' with 'throw' on dwitter and it will print the source bellow the dweet.
- gregsadetsky 6y agoWhat @tomxor said! To try it out, go to https://example.com https://example.com and run the following setup code first: c = document.createElement('canvas'); document.body.appendChild(c); x = c.getContext('2d'); S = Math.sin; T = Math.tan; C = Math.cos; t = 0; You will then be able to paste the tweet code `eval(unescape(...` into the console and see one frame rendered. :-)
- girvo 6y agoYou can also go to data:text/html,<body> instead of example.com (if I got the syntax right for the data URI, I’m on mobile)
- gregsadetsky 6y agoJust tried, that works too! Thanks
- fishtoaster 6y agodtwitter does some setup beforehand to make this work - creating a canvas, saving a few math functions, etc. Here's a jsfiddle with those set up if you want to play with it. https://jsfiddle.net/2mt94x5n/ https://jsfiddle.net/2mt94x5n/
- app4soft 6y agoJust curious, is it possible add execution of 140 chars snippets (in JS, Py, etc.) as feature to Nitter?[0] [0] https://github.com/zedeus/nitter https://github.com/zedeus/nitter
- CapsAdmin 6y agoThanks! I tried deobsfucating as much as I could, but I had to go. https://jsfiddle.net/omgz5xjd/16/ https://jsfiddle.net/omgz5xjd/16/ I also missed tomxor's explanation which made things more clear. Initially I thought there was some sort of scanline thing going on.
- CapsAdmin 6y agoops, wrong url, this is the correct one https://jsfiddle.net/capsadmin/n1ovq5fw/ https://jsfiddle.net/capsadmin/n1ovq5fw/
- PTOB 6y agoI love it. And it only uses 61MiB of RAM!
- datalist 6y agoDifficult to tell if sarcasm or not ;)
- tomxor 6y agoI was wondering this too :) But I can accept the comment as valid interpreted either way. On the one hand 61MB feels like a lot for such a tiny piece of source code, but then we know there is a massive heap of runtime it's relying on in the browser, JS engine, canvas implementation etc etc. Then again, an average "modern" website using 61MB isn't considered all that much for one page! Some of that is surely the rest of Dwitter itself, but probably not a huge portion.
- bartread 6y agoAnd an entire CPU core. I still love it as well though.
- app4soft 6y agoI don't like JS on web & desktop, but this one[0] is cute. [0] https://www.dwitter.net/d/7063 https://www.dwitter.net/d/7063
- hombre_fatal 6y agoIs the disclaimer really necessary or are you making a joke about how people can't even share the smallest of things without telling us they dislike Javascript?
- app4soft 6y ago> Is the disclaimer really necessary Yes.[0] [0] https://news.ycombinator.com/item?id=16191843 https://news.ycombinator.com/item?id=16191843
- postalrat 6y agoWhat?[0] [0] https://news.ycombinator.com/reply?id=25089857 https://news.ycombinator.com/reply?id=25089857
- 14u2c 6y agoOh please. Is this suppose to be some sort of gotcha about the dangers of using JavaScript? I don't see how the existence of hardware level (CPU) vulnerability that could impact any software, including the browser itself, applies here. If you refuse to use a modern web browser, that's a totally valid choice, but doing it based on some misguided notion of security is nonsensical.
- app4soft 6y ago> but doing it based on some misguided notion of security What browser you use? I suppose it is Chrome/Chromuim-based browser of Firefox. Did you read its changelogs? There are a lot of JavaScript-related vulnerability issues fixed this year (as in previous 10 years), right?
- holler 6y agonever heard of this website, really cool! and amazed at what you can do with 140 chars of javascript and some math :D
- btbuildem 6y agoNice to see that the spirit of mid-90s demo scene never really died.
- deleted 6y ago[deleted]
- downsouthkakona 6y agoThis is actually really cool, nice work komrade
- wojciii 6y agoAnd let's not forget 100-200 MiB of browser and OS. This is truly amazing in terms of space. /Rant
- Dylan16807 6y agoYou could write a compatible renderer in a few KB or less.
- tomxor 6y agoYou are technically correct, but missing the point. It's an artificial constraint in a common context for fun. The other problem with this argument is that it never ends: If you write on "bare metal" one can argue you are relying on thousands if lines of firmware and microcode. Then you go even lower and one can argue that your high level CISC ISA is doing too much work for you. Eventually you end up with transistors and then people will end up arguing which transistor is the least complex design to fabricate. I will admit trying to compute with sticks, stones and crabs (for real) is a fun idea, but it's hard to achieve much beyond a ripple adder.
- convolvatron 6y agodo you have a picture or writeup of a crab adder?
- tomxor 6y agoI was refering to this article [0] where some computer scientists test out swarm crab computing in Japan, it's a fun read :) you can get the full text using reader mode in firefox. [0] https://www.technologyreview.com/2012/04/12/186779/computer-scientists-build-computer-using-swarms-of-crabs/ https://www.technologyreview.com/2012/04/12/186779/computer-... I wish I had enough time (and crabs) to try this myself.
- trollied 6y agoCheck out ShaderToy if you haven't seen it before: https://www.shadertoy.com/browse https://www.shadertoy.com/browse
- nbevans 6y agoThat's cool. Now make it render the effects of a blackhole on a stargate wormhole ala SG-1 episode: "A Matter of Time".
- NiceWayToDoIT 6y agoWhat is exactly 140 chr as on that page following script does the drawing? <script> function createStats() { var stats = new Stats(); stats.showPanel(0); stats.dom.setAttribute('id', 'stats'); document.body.appendChild(stats.dom); return stats; } var stats = createStats(); setStatsVisibility(false); var c = document.querySelector("#c"); c.width = 1920; c.height = 1080; var x = c.getContext("2d"); setupPolyfills(x); var S = Math.sin; var C = Math.cos; var T = Math.tan; function R(r,g,b,a) { a = a === undefined ? 1 : a; return "rgba("+(r|0)+","+(g|0)+","+(b|0)+","+a+")"; } var time = 0; var frame = 0; var nextFrameMs = 0; newCode(src); function loop(frame_time) { stats = stats || createStats(); if (playing){ requestAnimationFrame(loop); epsilon = 1.5; // Acounts for different timestamp resolution and slight jitter if (frame_time < nextFrameMs - epsilon) { return; // Skip this cycle as we are animating too quickly. } nextFrameMs = Math.max(nextFrameMs + 1000 / FPS, frame_time); } time = frame/FPS; if(time * FPS | 0 == frame - 1){ time += 0.000001; } frame++; try { stats.begin(); if (window.navigator.userAgent.indexOf("Edge") > -1 && (u + "").match(/c\s*\.\s*(width|height)\s*(=|\+=|-=|\*=|\/=|%=|\**=|<<=|>>=|>>>=|&=|\^=|\|=)/) != null) { x.beginPath(); x.resetTransform(); x.clearRect(0, 0, c.width, c.height); } u(time); if (recording && frame % 2 == 0) { gifctx.fillStyle = "white"; gifctx.fillRect(0, 0, gifCanvas.width, gifCanvas.height); gifctx.drawImage(c, 0, 0, gifCanvas.width, gifCanvas.width / c.width * c.height); gifctx.font = "20px sans-serif"; gifctx.strokeStyle = "white"; gifctx.fillStyle = "black"; gifctx.miterLimit = 2; gifctx.lineJoin = 'circle'; gifctx.lineWidth = 3; gifctx.strokeText("dwitter.net/d/" + dweetId, 3, gifCanvas.height - 16); gifctx.lineWidth = 1; gifctx.fillText("dwitter.net/d/" + dweetId, 3, gifCanvas.height - 16); var usernameWidth = gifctx.measureText("u/" + username).width; gifctx.lineWidth = 3; gifctx.strokeText("u/" + username, gifCanvas.width - usernameWidth - 3, gifCanvas.height - 16); gifctx.lineWidth = 1; gifctx.fillText("u/" + username, gifCanvas.width - usernameWidth - 3, gifCanvas.height - 16); encoder.addFrame(gifctx, { copy: true, delay: 1000 / 30 }); } stats.end(); displayError(""); } catch (e) { stats.end(); displayError(e); throw e; } } if(autoplay) { loop(0.0); } function reset(){ if (browserGlobals) { Object.keys(window).forEach(key => { if (!browserGlobals[key]) { console.log("Removing new global: " + key); delete window[key]; } }); } c = document.querySelector("#c"); c.width = 1920; c.height = 1080; S = Math.sin; C = Math.cos; T = Math.tan; R = function(r,g,b,a) { a = a === undefined ? 1 : a; return "rgba("+(r|0)+","+(g|0)+","+(b|0)+","+a+")"; }; x = c.getContext("2d"); setupPolyfills(x); time = 0; frame = 0; } function setupPolyfills(x) { if (typeof x.resetTransform === "undefined") { x.resetTransform = function() { this.setTransform(1, 0, 0, 1, 0, 0); }; } if (typeof x.ellipse === "undefined") { x.ellipse = function(x, y, rx, ry, rotation, startAngle, endAngle, antiClockwise) { this.save(); this.translate(x, y); this.rotate(rotation); this.scale(rx, ry); this.arc(0, 0, 1, startAngle, endAngle, antiClockwise); this.restore(); }; } // Internet Explorer Math stuff // Mostly from https://www.developer.mozilla.org let polyfills = { cosh: function(v) { return (Math.pow(Math.E, v) + Math.pow(Math.E, -v)) / 2; }, acosh: function(v) { return Math.log(v + Math.sqrt(v * v - 1)); }, asinh: function(x) { if (x === -Infinity) { return x; } else { return Math.log(x + Math.sqrt(x * x + 1)); } }, atanh: function(x) { return Math.log((1+x)/(1-x)) / 2; }, cbrt: function(x) { var y = Math.pow(Math.abs(x), 1/3); return x < 0 ? -y : y; }, clz32: function(x) { if (x == null || x === 0) { return 32; } return 31 - Math.floor(Math.log(x >>> 0) * Math.LOG2E); }, expm1: function(x) { return Math.exp(x) - 1; }, fround: (function (array) { return function(x) { return array[0] = x, array[0]; }; })(new Float32Array(1)), hypot: function() { var y = 0, i = arguments.length; while (i--) y += arguments[i] * arguments[i]; return Math.sqrt(y); }, imul: function(a, b) { var aHi = (a >>> 16) & 0xffff; var aLo = a & 0xffff; var bHi = (b >>> 16) & 0xffff; var bLo = b & 0xffff; return ((aLo * bLo) + (((aHi * bLo + aLo * bHi) << 16) >>> 0) | 0); }, log1p: function(x) { return Math.log(1 + x); }, log2: function(x) { return Math.log(x) * Math.LOG2E; }, log10: function(x) { return Math.log(x) * Math.LOG10E; }, sign: function(x) { return ((x > 0) - (x < 0)) || +x; }, sinh: function(x) { return (Math.exp(x) - Math.exp(-x)) / 2; }, tanh: function(x){ var a = Math.exp(+x), b = Math.exp(-x); return a == Infinity ? 1 : b == Infinity ? -1 : (a - b) / (a + b); }, trunc: function(v) { v = +v; if (!isFinite(v)) return v; return (v - v % 1) || (v < 0 ? -0 : v === 0 ? v : 0); } }; for (var key in polyfills) { if (polyfills.hasOwnProperty(key)) { if (typeof Math[key] === "undefined") { Math[key] = polyfills[key]; } } } } // Remember the default globals so that later we can remove any that // were created by the dweet browserGlobals = {}; Object.keys(window).forEach(key => { browserGlobals[key] = true; }); </script>
- littlecranky67 6y agoThis is nice! One thing, I try to copy/paste the code from this dweet and paste into my VSCode instance - it immetiately crashes (macOS). Anyone has the same issue? It only seems to crash when I start copying starting at egal - if I include the function() part it does not crash?
- tomxor 6y agoStrange. It's possible the syntax highlighter is getting stuck on the UTF-16 surrogate pairs. Does it happen if you change the language to plain text before pasting in? alternatively try pasting in the decoded version (you can get it by replacing 'eval' with 'throw' in dwitter)... if the later still causes it to hang then It must be attempting to evaluate it!