The magician's rabbit has escaped the stage with its hat and is looking for his carrot in a strange forest... Move with arrows / WASD / ZQSD. Read the demo details if it looks bad!
for(_="onkeyJstyleI I=WitionVJupUWposV:fixed;C<a UCBatAin@MAh.s@(SSf/5)RrotAeQQX(PforO||(Nem${ti+j}scale(this[`.5padd@g:-24@}@,*3+.@nerHTML+=(d-u)/9*Omtrans.I.=`'' id=;background:#font:<div rad))QZ(-a}P5.navigAor.plA[x=y=a=5]?2:=e=>lurdl*d*l*ur*u`[(e.which+3)%20]]=lAe3D(`]B3;2+0+0tfNJdown1,U0,b=` Wheight:95vh;perspective:7@;overflow:hidden6e0> W-I:preserve-3d;marg@:50vh+50%s>515>π₯10>π010>π40>π012>π07>π₯59>π¦46>π’214>π33>πBwidth:14};3t>π° W1.2/9};:-1)>π©`);O(i @ m=[5120,22511,29732,13540,29829,57087])O(j=16;j--;)fNs+=m[i]>>j&1?` C11@961;:ji0)>`:q++&&q%7?` C7>`+[...`π³π²π΅π΄`][q%4]:``),&&(j1i12@QY(.U?R/5:0}2)`,x>46?(V=.U?a=0:sV=`5s`,sP,,)`):sP.7QZ(a}-x-y0)`);v=x,w=y,xSa+=(l-r)/30),yMAh.cos(a),x>0&&y>0&&m[~~(y/3)]>>x/3&1Nx=v,y=w),txy3+R}`,f++";G=/[-N-S@-CU-WIJ]/.exec(_);)with(_.split(G))_=join(shift(q=u=d=l=r=f=0));setInterval(_,33)<3
Zm9yKF89Im9ua2V5SnN0eWxlSSBJPVdpdGlvblZKdXBVV3Bvc1Y6Zml4ZWQ7QzxhIFVDQmF0QWluQE1BaC5zQChTU2YvNSlScm90QWVRUVgoUGZvck98fChOZW0fJHsedB5pK2p9HXNjYWxlKBx0aGlzW2AbLjUacGFkZEBnOhktMjRAGH1ALBcqMysWLkBuZXJIVE1MFSs9KGQtdSkvOSoUT20TdHJhbnMSLkkuEhEREz1gEB8nJw8gaWQ9DjtiYWNrZ3JvdW5kOiMMZm9udDoLPGRpdglyYWQpCClRWigeLWF9CFA1CAcXHgYuHm5hdmlnQW9yLnBsQRNbeD15PWE9NV0/MjoFPWU9PhtsdXJkbCpkKmwqdXIqdWBbKGUud2hpY2grMyklMjBdXT0EEmxBZTNEKAMbHWBdAkILMw87GTIfKzArMA50AWZOSmRvd24EMSxVBDAsYhU9YAlXaGVpZ2h0Ojk1dmg7cGVyc3BlY3RpdmU6N0A7b3ZlcmZsb3c6aGlkZGVuDDZlMD4JVxITLUk6cHJlc2VydmUtM2Q7bWFyZ0A6NTB2aCs1MCUOcz4BNTE1PvCfpZUBMTA+8J+QjAEwMTA+8J+QkgE0MD7wn5CNATAxMj7wn5CCATA3PvCfkKUBNTk+8J+mjQE0Nj7wn5CiATIxND7wn5CYATMzPvCfkIZCd2lkdGg6MQU0fR87CzMPDnQ+8J+QsAlXCzEuMh8vBTl9DzsSEzocLTEpPvCfjqlgKTtPKGkgQCBtPVs1MTIwLDIyNTExLDI5NzMyLDEzNTQwLDI5ODI5LDU3MDg3XSlPKGo9MTY7ai0tOylmTnMVKz1tW2ldPj5qJjE/YAlDGTEaMUAMOTYxOxITOgMeahYaBmkWGhcwKT5gOnErKyYmcSU3P2AJQws3Dw4dPmArWy4uLmDwn4yz8J+MsvCfjLXwn4y0YF1bcSU0XTpgYCksAiYmKAIQAx5qFjEaBmkWMRcyQAdRWSgeAi5VP1IvNTowfQgcMhopYCx4PjQ2PygCEVY9Ai5VP2E9MDpzEVY9YDVzYCxzEFAaCAMYLBgsGClgKTpzEFAuNwhRWigeYX0IAx4teAYteRcwKWApO3Y9eCx3PXkseBRTYSs9KGwtcikvMzApLHkUTUFoLmNvcyhhKSx4PjAmJnk+MCYmbVt+fih5LzMpXT4+eC8zJjFOeD12LHk9dyksdBADHngGeQYzK1J9HwdgLGYrKyI7Rz0vWwEtH04tU0AtQ1UtV0lKXS8uZXhlYyhfKTspd2l0aChfLnNwbGl0KEcpKV89am9pbihzaGlmdChxPXU9ZD1sPXI9Zj0wKSk7c2V0SW50ZXJ2YWwoXywzMyk8Mw==
// INIT:
// Tree counter
q =
// Up / down / left / right keys pressed
u = d = l = r =
// Frame counter
f = 0;
// Game loop
// ---------
setInterval(e => {
// At the beginning of the demo, f == 0, then f is incremented at each frame.
// So the following code is executed once:
f ||
(
// KEYBOARD INPUT
/*
The rabbit is controlled with the arrow keys, or WASD, or ZQSD.
up (u), right (r), down (d) and left (l) are initialized at 0.
When a key is down, one of these global vars is set to 1,
according to the current keyCode (also called e.which).
Dir. | key | which | (which+3)%20
------+-----+-------+-----
Up | β | 38 | 1
| W | 90 | 12
| Z | 87 | 10
------+-----+-------+----
Right | β | 39 | 2
| D | 68 | 11
------+-----+-------+----
Down | β | 40 | 3
| S | 83 | 6
------+-----+-------+----
Left | β | 37 | 0
| A | 65 | 8
| Q | 81 | 4
The string 'lurdl*d*l*ur*u' is built from each value of "(e.which+3)%20".
When a key is down, the corresponding global is set to 1.
When a key is up, it is set to 0.
The code below can't be merged into:
"onkeydown=onkeyup=e=>this[`lurdl*d*l*ur*u`[(e.which+3)%20]]^=1"
Because multiple keydown events are triggered when a key is pressed for a few seconds.
*/
onkeydown = e => this['lurdl*d*l*ur*u'[(e.which + 3) % 20]] = 1,
onkeyup = e => this['lurdl*d*l*ur*u'[(e.which + 3) % 20]] = 0,
// HTML/CSS
// HTML code is inserted in the body element (called "b").
b.innerHTML =
// The scene container, used to set the viewport's height (95vh), perspective (7in) and background (green).
// overflow:hidden prevents to draw anything outside the viewport.
// Also, 1in = 96px, so it's handy to set big dimensions.
`<div style=height:95vh;perspective:7in;overflow:hidden;background:#6e0>`
// The scene (called "s"), used to force "preserve-3d" (otherwise, the rendering would be isometric),
// and place the content's transform-origin at the center of the viewport (top: 50vh, left: 50%).
// "margin:50vh+50%" is the same as "margin:50vh 50%", but without space.
// The lack of spaces in the style attribute allows to remove attribute quotes.
+ `<div style=transform-style:preserve-3d;margin:50vh+50% id=s>`
// The carrot and the 9 animals:
// They're placed in <a> elements because consecutive <a>'s don't require closing tags (</a>).
// The id of these elements have the form t${y}${x}.
// It's similar to the trees' ids, because animals and trees have a very similar behavior.
// The onkeyup attribute is only used to differenciate trees (that don't have one) and animals.
// position:fixed is used to place these elements on the top-left corner of the scene.
// "font:3em''" is the same as "font:3em sans-serif", but shorter and without spaces.
// The padding-top is used to place these elements 2em lower than the trees, so they can touch the ground.
// Also, 1em ~= 12px, so it's handy to set the font-sizes and margins with only 1 digit.
+
`<a onkeyup style=position:fixed;font:3em'';padding:2em+0+0 id=t515>π₯
<a onkeyup style=position:fixed;font:3em'';padding:2em+0+0 id=t10>π
<a onkeyup style=position:fixed;font:3em'';padding:2em+0+0 id=t010>π
<a onkeyup style=position:fixed;font:3em'';padding:2em+0+0 id=t40>π
<a onkeyup style=position:fixed;font:3em'';padding:2em+0+0 id=t012>π
<a onkeyup style=position:fixed;font:3em'';padding:2em+0+0 id=t07>π₯
<a onkeyup style=position:fixed;font:3em'';padding:2em+0+0 id=t59>π¦
<a onkeyup style=position:fixed;font:3em'';padding:2em+0+0 id=t46>π’
<a onkeyup style=position:fixed;font:3em'';padding:2em+0+0 id=t214>π
<a onkeyup style=position:fixed;font:3em'';padding:2em+0+0 id=t33>π`
// The rabbit, called "t".
// navigator.platform[5] is true on Mac OS and false on Windows and Linux.
// On Mac OS, the rabbit's width is set to 1.2em. On other platforms, 1.4em.
// Mac OS emoji are not rendered with the same alignment,
// so this custom width allows the rabbit's head to be right above the hat.
// Also, x, y and a (the rabbit's position in inches and angle in radians) are initialized at the value "5".
+ `<a onkeyup style=position:fixed;width:1.${navigator.platform[x = y = a = 5] ? 2 : 4}em;font:3em'' id=t>π°`
// The rabbit's hat, reversed with "scale(-1)".
// The hat's line-height is .2em on MacOS and .9em on other platforms
// On most browsers, the final parenthesis of a CSS rule can be omitted, like: "scale(-1"
// But they need to be present in order to support Safari.
+ `<div style=font:.6in/.${navigator.platform[x = y = a = 5] ? 2 : 9}em'';transform:scale(-1)>π©`
);
// MAP
// The map (called "m") is encoded in binary: 1 = path, 0 = trees.
// It is mirrored horizontally, because it's read from right to left:
// 0001010000000000
// 0101011111101111
// 0111010000100100
// 0011010011100100
// 0111010010000101
// 1101111011111111
// For each line (i)...
for(i in m = [5120, 22511, 29732, 13540, 29829, 57087])
// For each column (j)...
for(j = 16; j--;)
// If the game has started (f > 0):
f ||
(
// Insert the ground tiles and the trees in the scene.
s.innerHTML +=
// If the j'th bit of the i'th value of m is equal to 1:
m[i] >> j & 1
// Add a brown ground tile:
// "padding:1.51in" sets the width and the height to 3.02in. They overlap a little to avoid small gaps on some browsers.
// They're placed at X=(j*3+.5)in and Y=(i*3+.5)in to be well adjusted to the rabbit's collisions.
// The ground could have been made of just one element with multiple box-shadows,
// but on most browsers, the result was too glitchy on many browsers, so I didn't keep it.
? `<div style=position:fixed;padding:1.51in;background:#961;transform:translate3D(${j * 3 + .5}in,${i * 3 + .5}in,0)>`
// if current bit is 0:
// Increment the tree counter (q), and test if q % 7 > 0.
// This test allows to remove a few trees from the scene, and stay below 100 elements.
: q++ && q % 7
// Add a tree as a <div> element, with the id `t${i+j}`.
// NB: i and j are not summed, because i is a string. ex: t00, ..., t015, t10, ..., t15, etc.
? `<div style=position:fixed;font:7em'' id=t${i + j}>`
// The (q%4)'th tree is chosen from an array of 4 emoji trees,
// This array is built with a string and the Unicode-aware ES6 spread operator "...".
+ [...`π³π²π΅π΄`][q % 4]
// else, add nothing.
: ``
),
// If a tree, animal or carrot exists at the line i and column j:
this[`t${i + j}`] && (
// Place the current tree or animal at the right position on the scene:
// translateX: j*3+1.5, translateY: i*3+1, translateZ: 2.
// rotateZ: -a (to cancel the scene rotation and face the camera).
// rotateX: 5rad (to be less vertical and more slanted towards the camera).
// rotateY: TODO
this[`t${i + j}`].style.transform=`translate3D(${j * 3 + 1.5}in,${i * 3 + 1}in,2in)rotateZ(${-a}rad)rotateX(5rad)rotateY(${
// trees have no onkeyup attribute, but the animals and carrot have one.
// This difference is used to animate all the objects that are not trees:
// rotateY: Math.sin(t/5)/5.
this[`t${i + j}`].onkeyup ? Math.sin(f / 5) / 5 : 0
}rad)scale(2.5)`,
// ENDING
// If the rabbit's x is over 46in (on the rightmost column, where the carrot is), play the final animation:
x > 46 ? (
// Set a CSS transition to all the objects in the scene:
this[`t${i + j}`].style.transition =
// If the object is an animal or a carrot:
this[`t${i + j}`].onkeyup
// Set a 0-second transition to the current element and reset the angle a to the value 0.
// Animals can't have a CSS transition because their rotateY changes at each frame.
? a = 0
// If the object is a tree:
// Set a 5second transition to the current tree and to the whole scene.
: s.style.transition = `5s`,
// Place the scene far from the camera and almost vertical to see the full path centered on the screen (with the surprise!)
s.style.transform = `rotateX(.5rad)translate3D(-24in,-24in,-24in)`
)
// Otherwise, just rotate the scene according to the angle a, and move it according to x and y.
: s.style.transform = `rotateX(.7rad)rotateZ(${a}rad)translate3D(${-x}in,${-y}in,0)`
);
// GAMEPLAY
// backup the value of x and y (the rabbit's position) into v and w.
v = x,
w = y,
// Update the rabbit's x, y and angle (a) according to the keys pressed:
// the angle a is incremented or decremented of 1/30 radian with left and right keys.
// Then x and y are incremented or decremented of 1/9 inch along this angle with up and down keys.
// The coefficients Math.cos(a) and Math.sin(a) are applied to x and y, in order to move the right distance in the current angle.
// (d-u) and (l-r) are equal to 1 or -1 according to the keys pressed.
// If no key is pressed, (d-u) and (l-r) are equal to zero, so the rabbit doesn't move.
x += (d - u) / 9 * Math.sin(
a += (l - r) / 30
),
y += (d - u) / 9 * Math.cos(a),
// Prevent the rabbit from going out of bounds (negative x or y) or out of the brown path:
// Check if x > 0, y > 0 or if the tile below the rabbit belongs to the path.
// The tiles measure 3in * 3in, so we're on the path if the ~~(x/3)th bit of the ~~(y/3)th value of m is equal to 1).
// If one of these three conditions is not fullfilled, revert x and y to their backup values.
// This is how the game's collisions work.
// NB: the right shift operator (>>) automatically floors the value of x/3, so we don"t have to redo it.
x > 0 && y > 0 && m[~~(y / 3)] >> x / 3 & 1 || (
x = v,
y = w
),
// Place the rabbit on the scene according to x, y and a.
// The Z coordinate is computed from the frame counter: 3 + Math.sin(f/5):
// This makes the rabbit jump at the right speed and height, in em.
// the final rotateX(5rad) is used to make the rabbit less vertical, an