From 73e1530a1d2072e9a4d336108e6c3693cbdb645e Mon Sep 17 00:00:00 2001 From: B Wu Date: Fri, 9 Aug 2024 21:15:49 -0700 Subject: [PATCH] feat: add in creature rendering --- static/index.js | 220 +++++++++++++++++++++++++++++++++- static/sprites/defaults/1.gif | Bin 0 -> 6512 bytes static/style.css | 7 ++ templates/index.html | 1 + 4 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 static/sprites/defaults/1.gif create mode 100644 static/style.css diff --git a/static/index.js b/static/index.js index 2c8ed38..e1c4d35 100644 --- a/static/index.js +++ b/static/index.js @@ -1,5 +1,217 @@ -window.onload = () => { - console.log('from js'); - const kennelWindowEle = document.querySelector('#kennel-window'); - kennelWindowEle.innerHTML = 'rendered from static/index.js'; +const CREATURE_HEIGHT = 32; // in px +const CREATURE_WIDTH = 32; // in px +const FRAME_RATE = 12; // in FPS +const FRAME_DELAY = 1 / FRAME_RATE * 1000; // in ms +const DEFAULT_SPRITE_SHEET_COUNT = 1; // number of default sprite sheets + +/** + * Enum of creature states + * @readonly + * @enum {number} + */ +const CreatureState = Object.freeze({ + IDLE: 0, + ALERT: 1, + SCRATCH_SELF: 2, + SCRATCH_NORTH: 3, + SCRATCH_SOUTH: 4, + SCRATCH_EAST: 5, + SCRATCH_WEST: 6, + TIRED: 7, + SLEEPING: 8, + WALK_NORTH: 9, + WALK_NORTHEAST: 10, + WALK_EAST: 11, + WALK_SOUTHEAST: 12, + WALK_SOUTH: 13, + WALK_SOUTHWEST: 14, + WALK_WEST: 15, + WALK_NORTHWEST: 16, +}) + +/** + * @typedef {[number, number]} SpriteFrameOffset the offset of the sprite with respect to + * the left/top background position offset (off by factor of sprite size) + * @type {Object.>} + */ +const CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES = Object.freeze({ + [CreatureState.IDLE]: [ + [-3, -3] + ], + [CreatureState.ALERT]: [ + [-7, -3] + ], + [CreatureState.SCRATCH_SELF]: [ + [-5, 0], + [-6, 0], + [-7, 0], + ], + [CreatureState.SCRATCH_NORTH]: [ + [0, 0], + [0, -1], + ], + [CreatureState.SCRATCH_SOUTH]: [ + [-7, -1], + [-6, -2], + ], + [CreatureState.SCRATCH_EAST]: [ + [-2, -2], + [-2, -3], + ], + [CreatureState.SCRATCH_WEST]: [ + [-4, 0], + [-4, -1], + ], + [CreatureState.TIRED]: [ + [-3, -2] + ], + [CreatureState.SLEEPING]: [ + [-2, 0], + [-2, -1], + ], + [CreatureState.WALK_NORTH]: [ + [-1, -2], + [-1, -3], + ], + [CreatureState.WALK_NORTHEAST]: [ + [0, -2], + [0, -3], + ], + [CreatureState.WALK_EAST]: [ + [-3, 0], + [-3, -1], + ], + [CreatureState.WALK_SOUTHEAST]: [ + [-5, -1], + [-5, -2], + ], + [CreatureState.WALK_SOUTH]: [ + [-6, -3], + [-7, -2], + ], + [CreatureState.WALK_SOUTHWEST]: [ + [-5, -3], + [-6, -1], + ], + [CreatureState.WALK_WEST]: [ + [-4, -2], + [-4, -3], + ], + [CreatureState.WALK_NORTHWEST]: [ + [-1, 0], + [-1, -1], + ], +}); + +/** + * Properties for creatures running around the screen + * @typedef {Object} Creature + * @property {string} name the name of the creature, used as the HTML element's id + * @property {string} spriteSheet the file name of the sprite sheet. should exist in {@link /static/sprites} + * @property {number} state the current state of the creature (should be member of {@link CreatureState} enum) + * @property {number} stateDuration the number of frames the creature has been in its current state + * @property {number} positionX the number of pixels away from the left side of the container element + * @property {number} positionY the number of pixels away from the top of the container element + * @property {HTMLElement} element the HTML element rendering the creature in the DOM + * @property {HTMLElement} container the HTML element containing the creature (the kennel, if you will) + */ + +/** + * Returns random number between min (inclusive) and max (exclusive) + * If max is less than or equal to min, return min + * @param {number} min inclusive lower bound + * @param {number} max exclusive upper bound + * @return {number} number in [min, max) + */ +const randomInt = (min, max) => { + if (max <= min) return min; + return Math.floor(Math.random() * (max - min) + min); +} + +/** + * Return a creature's next frame based on the current creature frame + * @param {Creature} creature + * @return Creature the next frame of the creature + */ +const getNextCreatureFrame = (creature) => { + // TODO + return { ...creature, stateDuration: creature.stateDuration + 1 }; +} + +/** + * Render a frame of the creature and loads the next frame for render + * @param {Creature} creature + * @return Creature the next frame of the creature + */ +const renderCreature = (creature) => { + // set position + const positionX = Math.min(creature.positionX, creature.container.clientWidth); + const positionY = Math.min(creature.positionY, creature.container.clientHeight); + creature.element.style.setProperty('left', `${positionX}px`); + creature.element.style.setProperty('top', `${positionY}px`); + + // set sprite + const spriteFrames = CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES[creature.state] + const currentSpriteFrameOffset = spriteFrames?.[creature.stateDuration % spriteFrames.length] + creature.element.style.setProperty( + 'background-position', + `${currentSpriteFrameOffset[0] * CREATURE_WIDTH}px ${currentSpriteFrameOffset[1] * CREATURE_HEIGHT}px` + ) + + const nextCreatureFrame = getNextCreatureFrame(creature); + setTimeout(renderCreature, FRAME_DELAY, nextCreatureFrame) +} + +/** + * Create the creature and start its rendering + * @param {HTMLElement} container container element for creatures. the kennel if you will + * @param {string} name name of the creature + * @param {string} [spriteSheet] name of the sprite sheet. must be in /static/sprites + * uses default sprite sheet if undefined + * @param {number} [initialState] starting state of the creature + * @param {number} [initialPositionX] initial x position in pixels (from the left side) + * @param {number} [initialPositionY] initial y position in pixels (from the top) + */ +const createCreature = ( + container, + name, + spriteSheet, + initialState = CreatureState.IDLE, + initialPositionX = 0, + initialPositionY = 0 +) => { + const creatureEl = document.createElement('div'); + const spriteSheetUrl = spriteSheet + ? `url('/static/sprites/${spriteSheet}')` + : `url('/static/sprites/defaults/${randomInt(1, DEFAULT_SPRITE_SHEET_COUNT)}.gif')`; + + creatureEl.setAttribute('id', name); + creatureEl.style.setProperty('width', `${CREATURE_WIDTH}px`); + creatureEl.style.setProperty('height', `${CREATURE_HEIGHT}px`); + creatureEl.style.setProperty('position', 'fixed'); + creatureEl.style.setProperty('image-rendering', 'pixelated'); + creatureEl.style.setProperty('background-image', spriteSheetUrl); + + container.appendChild(creatureEl); + + renderCreature({ + name, + spriteSheet, + state: initialState, + stateDuration: 0, + positionX: Math.max(0, initialPositionX), + positionY: Math.max(0, initialPositionY), + element: creatureEl, + container + }); +} + +window.onload = () => { + const kennelWindowEle = document.querySelector('#kennel-window'); + createCreature( + kennelWindowEle, + 'test-creature', + undefined, + CreatureState.SCRATCH_SELF + ) } diff --git a/static/sprites/defaults/1.gif b/static/sprites/defaults/1.gif new file mode 100644 index 0000000000000000000000000000000000000000..0d264d87d0b9106914df8d1475d80b978a87e609 GIT binary patch literal 6512 zcmV-$8IR^iNk%w1VE_Su0Q3L=0000B2nReS8CE_mT|YZ>1#@drNo`h2fNETdd1;u4 zdB=)z+N+QMvj6tn$@JjT_wC;G>+Apd@c;bq^#A_=0000000000000000000000000 z000000000000000A^8LW00000EC2ui0004i000I5ARvw;5S8SKu59bRa4gSsZQppl z3c`J=qn~a_EEb1q{1l9Gi0 zT|Ip+IM4|b%a(W+1fXD)!by3k6v=SL?4(JnbAtM`0-*p*jU@yGcp^~>zEg)5?o0%M zf!amc8YLw~p&x{I<+PHhsWgCHyjjI?eEWn!RjerQ(wspm;(~;R8;=c}R-)Up!#YX> z%hv6t$&`g5m^76rV53h7t2w{`aaNXs58+`z6q8qrt|pM0Nw&3Bi=-yF^;wjMWoE%n zO&~-#2{q3543QQAdM~-rZ&&y8YBI`neMGnCwK{$9D%zbQk5k;drrpHxhVLLyye8F3 zRLjY=S4O8ThMI6h(QZ?{ z1W!Deq{Kr%RxDFUVo)`A#EJg>0SrQ}OhF)bu7Sf?gc}}WmM4Tcw&8mS;6n#GG?3zq zUxuCN8FT58o43q_dB>`rI;fuU*#6})hZkY`n1d5nYkYe%JPP*+AvlMm>(mXl6+2Gv%0JG!hUr zP~c`Mv6)8kQ~?B1a%N7IwxDYWmxmyL(?3>>HV;k>FgN2MyZH3pJbBI2DLHOMyGU){jJ98hSuRlOGlL+w zNEzaiDj-pxEsQHEHYnYqennUE`sa-9f${zy>{r8RpAFpJIn zf%QOlzGv~KVfFV7S2mk$6IB);e6~J&i>#*AX;|?#1X|w>_`<^2OfbDr+jYsvB}VEq zl_*b5X(#Yvf^VV>L@mbuwRYd_c>*zp>E4f|`b>Blfg9qMO>IYch$;P$s(2z=zU*bX ztAGp4xQSAUdB_Z7JJHLKH&I*15id%7w4wL2?*h?hqxrLorQg93hqZju)(rZA^Q^C<#0qpto=(P;De}(IWI0j|o_; zFxc74P#71(2%SblSprcLJcj^3EsRSHte$qHk~HNtZUUCTMHIvG!E|kKgu{`F4l`IT z5iTr?@_WMiN`ejlA_6Kw;mQ@lVwi(b)l5F6q8YlfhX8l+=rpadPiQ(A1~*jT$GO|nJ+?cy;isVGZI;BV)SA?OD+ry+Gg5?7GKENJ3(aU++PDUZsx(GrS|*PS6h*FV z)H;o1CmwL2Me_mldg z5nRdzkgX*nUA=2wr{UPgwtyW$u&ib`%h_`jRtH(gEM_}PT0bbZv|^M_Wn+L0D~$FW zsEsW{CL4_~{EY~p5R(ieyIR?5_JY3k2~Kg4TV`lX1fp=ybgs zLFtUtLX#eKHdmaoV&W7J zk;h?)%j@>vewI4op#A<9u_{M)3WAX=r{(vq3{ z#5HF5rW7Rb;(9eD0cUb5@y&~39Q7kB~jJ{Ve)@bRCq>?B^*%_9-E3BejPXZDUC-l5CIu;86N5wn`*%G7P zTGx#g26+%V(yMD&hvjgwV3aPJqz@_CHozM8qnWyd8)l0ad zpi=9(?z%7mBg7j(UAjhVIL)7%X}f8ItrtRJt6l5W&#tU6)lgS3MEz2l4H0efoy10k zd7r0R+#W+7C?|U0$~La_28DZ~N%-o|zcdWD-3f~}2~^lu7_ISyILg=p#tC!(iDfKW z7_8@16NXg^Fd&+x@MVptEVj7ZWV30)5)EixO>n%gN?bxjJGX_fd)~4a=gLcv{4K|>5a8T+Wu)a7?5ARz4jTgJQG)f!Y(O1|-jh7mP(v_VO6R5jRE!!vmZ^Md)qhyEZG zej*Hz=0kvS5<<9nhBi|f1urQ$fqX?Dkj932F-3~RJ3OQqSmGQ1S5g2PXBvKiEx6!F zrIvE5M1}%2BQM8B=P*)JV|@aY9z9Y=ZiGkhhc;Z}QynxXO5iT8WMzqjU^}2K$j6E( zwp~498ivw7p{N9eC1Lqv6Xo&}&*u`7m=@wFM^R%$c2Y;^L`&ynK%BG%*K>=MHB6Ud zOdkbAnF5QPkx?mPh06$HthEF`P(tTYgJ&UFkTQTMqAmK8YU3n4b|On+;TY<~PTv@a z<6|rDlvnt)ip_{;Lz6wG)RAPhO$3>XD#-$^gh18P}F$MtY9OZy7c!4ZaX(kPJdLt887q^!EkclO^8D(Id`oK8zkaW732_3_psFrZkr%Artf% z2n{uO5_b;BQEB)k7CjOG>I5y=bEnm5Z)M;a>a~^wlzF5^gDe^gbd{c|d-_3#D*!}H5&&+%AZ_6n2*8Q|Wm%?yAY9rvNsQPdC3Om1VoGzyVLBsc z=OTQ8wIP{mkTWzCtkGA-HK`$n5E&R`KEWqm_GgB=1N7!`3KlG_E z#yB13VL9QZML0XABz++%OcK#yKw+Tx*okfU5MQPwwDf20!m4|rWF}K@fX6o70|@@b zYtpemY6&%!C><)JV?2`xmQZPy*-?%see*Ls(_=G5`Kc>02UJ%*0#&b}<1T7fA~|FW z7aJx$Cl62JKKgk;M1+IumSxe{EdNKLpSC^R<7Q#zLGL&kcXzRoka^A1XB%Ubz*=Y{ zw`g*;B9I7Pa4IDuqY1G9dG!)8@S=79uhcl|g)lH1Gu%q052RFFw?^J}4pN5~qFHrr zQwjfcqyXU>8HhJ_Cakv5HreQ5%+^VajLEAVmF?8 zF1;5+wQ!>^W=4U*0@#MNRv@l2v!fM-5R{WHX2URm(jWNAw;9JnP{DUm$}gX*96xi0 zeJL!&lPt-irBi_$a6=4Vc75y`s+`0yl~9CmiZ;9ZCVk?F>xeu(CmGH5nSjM57v^6} zH*iH+mtiq3XJ=f=ViBsbw|mO5KzOB%aw>eGT(0ps`1yw6n!0Njg_;5p#^)OEK#jsx zEso|sX9T^oi%3NTviXw@SviLPORKk*`4bU$Da<+zgF?0NwiQx?Jp%zR2RB5P;C&V| zy2j%a4YP|S*c4uNQ@D}7cM`gHLnMIqi_K6|@iMm4Ft@~sME0`bVEfatyiDWupFz%ZI?>h`s~mZuLym`TwLQkoSiycc^=Is?ZBJ6wzd zwgf~|95bAvdssRaOq|ttC&SdkvI9hgDzvC3dbL!F*B!G0|g+b_Vb5JfY2CI!kAML1xxYlB23<(ZW%F`staM2KR z$){C;f6l@b$_rk=y;B?xZpAx$Wh-ddM`YF?SpB&I zv#@f-nIQs{$JyYu_X>LAu(AO5U$#q&>f?WM@ZByX$$ugjDOKy? z!UYptT%mRU=3OIuaR4CwKW-ok`u(77^bARTM%sBu!r^6Xe4b5u5WbV$k7ZWv6p#bu zJ^t}x18&`WyOyWOVjDU(gNPb(yQP|aA@1kghZ$2eBg>DWZ!i~Q$JHP33)gW_*r2v0 zCZP-tu4Z+o1!iVg=`3|j5y22vTQR}Wf{@~?6Ls3#4_0H3CF+Ow_Y^8BE6+f2>m#%P z#&^n+;wsUEn(Wh)+iot7r|*FadsifsvTY)pr2xLr{$menRqN~bYrCmq@%4T zlKZCQG9rr=jhj8Ly~ZeX-laXGYjTpi zmTn0DL#kXR!<-mqTsIrMQF%B^%C9gGriI-VWs9#{A<_^ek4h!8g^O|N>4TZ>z0=^2 zq%a}IlK{Go(N`8}UnnM)5)11_mQHj!ItJ-5nmwlK8VQOGT>?6x_GwOnwbYZ~D^bON zKG92}aT{VwBpYDg!Q775V3?P+)c$KLt*)8!01c+?FCZE2ei+SuZmVXyLt>bvaq9tX zp&8p1${`jRQp^-r@K$`(924G;_MDRD>ZdEI>8gs2^8oT9zfAe78)EWNa_dMQgN*_6o$)qC2+Rw+2WyQ@A0|Rl<`8Ga`MpGJO!M8r$q6qcfv z;!`bWl>cPa=jnf53(HgQOTSyOQd|=^j#QdsCXiT%4N*qxYv=$_xoZmU2jWo9&;Sat za4@kYNxU@tt5+XPu(Bg^_Q?3`>-fLvezy|#fB%5HdwQR@07zrIiP;}ZtN68;DXpI; zPp5!wj>rbX_H6J5N+$@LpCz3yu)c!!*$5Cvl25t6WNC)-?xTTZ3f z8L`XY!Obh|BIDv?jAG<5&`+`tK<1W|mD?w1U^QFLfvuS=8DPLq00s0^EpOV-n!M6} zfd6-(1vA!|vUX385(GDpDO*;g8G&bCtbq}OkD3k~inI|D)CUeg7BmL`jzhuepvZ)= zWZCja@Kyi@{$%VMp=bw#mJV0^b2U@POinvTiYs`M*g1s;4j@Cx2nZ5GkZ{~?hgI@*A(#m5D-Gc;PA=-zkn(v>hBnbjc(>nU6!+O*V%xV8PJ_*3!25j{>E3 z$ny7R@fIKXhIiE+4EvkI+o2eV4IFcO^y)vW|7#jCPCYbq*C{gX^Z;KL0@X!bfn@n1 zT|ny*Ql3Wxq9+PBesD8XZ802}!W#buRdjidF(zIFu28fR>Ft(f-;WK5u}Q}K&B!MFz(mKVx0^ZRtzVI zrC))=A$496sHG)Si>u9W$@j0028r!J^;* literal 0 HcmV?d00001 diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..f77e958 --- /dev/null +++ b/static/style.css @@ -0,0 +1,7 @@ +#kennel-window { + width: 80vw; + min-width: 8rem; + height: 80vw; + min-height: 8rem; + margin: auto inherit; +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 1bb53e8..e36ba58 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,6 +4,7 @@ + Kennel Club