[{"data":1,"prerenderedAt":1321},["ShallowReactive",2],{"blog-canvas-sprite-physics":3},{"id":4,"title":5,"body":6,"description":15,"extension":1311,"meta":1312,"navigation":104,"path":1317,"seo":1318,"stem":1319,"__hash__":1320},"blog\u002Fblog\u002Fcanvas-sprite-physics.md","HTML5 Canvas: Sprite Animation and Game Physics",{"type":7,"value":8,"toc":1309},"minimark",[9,1305],[10,11,12,681],"i18n-text",{},[13,14,16,20,33,38,53,171,175,203,307,311,330,488,492,503,514,678],"template",{"v-slot:en":15},"",[17,18,5],"h1",{"id":19},"html5-canvas-sprite-animation-and-game-physics",[21,22,23,24,28,29,32],"p",{},"Now that we understand the Game Loop, inputs, and canvas transformations, it's time to put it all together. Let's look at how to build a complete ",[25,26,27],"strong",{},"Player Class"," using real production code as an example. We'll implement gravity for jumping and cycle through an array of images for ",[25,30,31],{},"Sprite Animation",".",[34,35,37],"h2",{"id":36},"_1-preparing-the-image-assets","1. Preparing the Image Assets",[21,39,40,41,45,46,45,49,52],{},"Instead of drawing a single square canvas shape, real games load sequential image items (frames). We load them carefully into memory and group them into Javascript objects based on the player's \"State\" (like ",[42,43,44],"code",{},"Idle",", ",[42,47,48],{},"Jump",[42,50,51],{},"MoveForward",").",[54,55,59],"pre",{"className":56,"code":57,"language":58,"meta":15,"style":15},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F Helper to generate the real DOM Image object\nfunction loadImg(src) {\n  const img = new Image();\n  img.src = src;\n  return img;\n}\n\n\u002F\u002F Group downloaded image arrays by player animation state\nfunction loadImagePlayer(country) {\n  const animations = { Idle: [], Jump: [], MoveForward: [] \u002F* ... *\u002F };\n  \n  \u002F\u002F Example: Loading 18 consecutive frames of Idle animation\n  for (let i = 0; i \u003C= 17; i++) {\n    \u002F\u002F String(i).padStart(2, \"0\") converts number 1 to exactly \"01\"\n    animations.Idle.push(loadImg(`.\u002FSprites\u002FCharacters\u002F${country}\u002FIdle\u002FIdle_0${String(i).padStart(2, \"0\")}.png`));\n  }\n  return animations;\n}\n","javascript",[42,60,61,69,75,81,87,93,99,106,112,118,124,130,136,142,148,154,160,166],{"__ignoreMap":15},[62,63,66],"span",{"class":64,"line":65},"line",1,[62,67,68],{},"\u002F\u002F Helper to generate the real DOM Image object\n",[62,70,72],{"class":64,"line":71},2,[62,73,74],{},"function loadImg(src) {\n",[62,76,78],{"class":64,"line":77},3,[62,79,80],{},"  const img = new Image();\n",[62,82,84],{"class":64,"line":83},4,[62,85,86],{},"  img.src = src;\n",[62,88,90],{"class":64,"line":89},5,[62,91,92],{},"  return img;\n",[62,94,96],{"class":64,"line":95},6,[62,97,98],{},"}\n",[62,100,102],{"class":64,"line":101},7,[62,103,105],{"emptyLinePlaceholder":104},true,"\n",[62,107,109],{"class":64,"line":108},8,[62,110,111],{},"\u002F\u002F Group downloaded image arrays by player animation state\n",[62,113,115],{"class":64,"line":114},9,[62,116,117],{},"function loadImagePlayer(country) {\n",[62,119,121],{"class":64,"line":120},10,[62,122,123],{},"  const animations = { Idle: [], Jump: [], MoveForward: [] \u002F* ... *\u002F };\n",[62,125,127],{"class":64,"line":126},11,[62,128,129],{},"  \n",[62,131,133],{"class":64,"line":132},12,[62,134,135],{},"  \u002F\u002F Example: Loading 18 consecutive frames of Idle animation\n",[62,137,139],{"class":64,"line":138},13,[62,140,141],{},"  for (let i = 0; i \u003C= 17; i++) {\n",[62,143,145],{"class":64,"line":144},14,[62,146,147],{},"    \u002F\u002F String(i).padStart(2, \"0\") converts number 1 to exactly \"01\"\n",[62,149,151],{"class":64,"line":150},15,[62,152,153],{},"    animations.Idle.push(loadImg(`.\u002FSprites\u002FCharacters\u002F${country}\u002FIdle\u002FIdle_0${String(i).padStart(2, \"0\")}.png`));\n",[62,155,157],{"class":64,"line":156},16,[62,158,159],{},"  }\n",[62,161,163],{"class":64,"line":162},17,[62,164,165],{},"  return animations;\n",[62,167,169],{"class":64,"line":168},18,[62,170,98],{},[34,172,174],{"id":173},"_2-the-player-class-structure","2. The Player Class Structure",[21,176,177,178,181,182,185,186,189,190,45,193,196,197,45,200,52],{},"Using a JavaScript ",[42,179,180],{},"class"," is the best standard to handle complex game entities. Our player needs properties mapping its physical location (",[42,183,184],{},"x, y","), movement momentum variables (",[42,187,188],{},"dx, dy","), animation status (",[42,191,192],{},"frame",[42,194,195],{},"state","), and world physics (",[42,198,199],{},"speed",[42,201,202],{},"gravity",[54,204,206],{"className":56,"code":205,"language":58,"meta":15,"style":15},"class Player {\n  constructor(country, position, ctx) {\n    this.ctx = ctx;\n    this.position = position; \u002F\u002F Is player on \"left\" or \"right\" side?\n    this.image = loadImagePlayer(country); \u002F\u002F Preload all sprite images\n    \n    \u002F\u002F Position and Physical Body Rules\n    this.size = 150;\n    this.x = position === \"left\" ? 300 : 600;\n    this.y = 0; \n    this.dy = 0; \u002F\u002F Delta Y: Vertical momentum velocity\n    this.speed = 5;\n    this.gravity = 0.2; \u002F\u002F Constant downward pulling force\n\n    \u002F\u002F Animation & State Control tracking\n    this.state = \"Idle\"; \u002F\u002F Default state\n    this.frame = 0; \u002F\u002F Current animation slide tracker\n    this.lastAnimate = Date.now(); \u002F\u002F Built-in timer to control frame pacing\n  }\n}\n",[42,207,208,213,218,223,228,233,238,243,248,253,258,263,268,273,277,282,287,292,297,302],{"__ignoreMap":15},[62,209,210],{"class":64,"line":65},[62,211,212],{},"class Player {\n",[62,214,215],{"class":64,"line":71},[62,216,217],{},"  constructor(country, position, ctx) {\n",[62,219,220],{"class":64,"line":77},[62,221,222],{},"    this.ctx = ctx;\n",[62,224,225],{"class":64,"line":83},[62,226,227],{},"    this.position = position; \u002F\u002F Is player on \"left\" or \"right\" side?\n",[62,229,230],{"class":64,"line":89},[62,231,232],{},"    this.image = loadImagePlayer(country); \u002F\u002F Preload all sprite images\n",[62,234,235],{"class":64,"line":95},[62,236,237],{},"    \n",[62,239,240],{"class":64,"line":101},[62,241,242],{},"    \u002F\u002F Position and Physical Body Rules\n",[62,244,245],{"class":64,"line":108},[62,246,247],{},"    this.size = 150;\n",[62,249,250],{"class":64,"line":114},[62,251,252],{},"    this.x = position === \"left\" ? 300 : 600;\n",[62,254,255],{"class":64,"line":120},[62,256,257],{},"    this.y = 0; \n",[62,259,260],{"class":64,"line":126},[62,261,262],{},"    this.dy = 0; \u002F\u002F Delta Y: Vertical momentum velocity\n",[62,264,265],{"class":64,"line":132},[62,266,267],{},"    this.speed = 5;\n",[62,269,270],{"class":64,"line":138},[62,271,272],{},"    this.gravity = 0.2; \u002F\u002F Constant downward pulling force\n",[62,274,275],{"class":64,"line":144},[62,276,105],{"emptyLinePlaceholder":104},[62,278,279],{"class":64,"line":150},[62,280,281],{},"    \u002F\u002F Animation & State Control tracking\n",[62,283,284],{"class":64,"line":156},[62,285,286],{},"    this.state = \"Idle\"; \u002F\u002F Default state\n",[62,288,289],{"class":64,"line":162},[62,290,291],{},"    this.frame = 0; \u002F\u002F Current animation slide tracker\n",[62,293,294],{"class":64,"line":168},[62,295,296],{},"    this.lastAnimate = Date.now(); \u002F\u002F Built-in timer to control frame pacing\n",[62,298,300],{"class":64,"line":299},19,[62,301,159],{},[62,303,305],{"class":64,"line":304},20,[62,306,98],{},[34,308,310],{"id":309},"_3-game-physics-gravity-and-jumping","3. Game Physics: Gravity and Jumping",[21,312,313,314,317,318,321,322,325,326,329],{},"In our ",[42,315,316],{},"update()"," method, we handle the classic Jump mechanic. Gravity constantly adds weight pushing the player down (",[42,319,320],{},"this.dy += this.gravity","), unless they touch the Ground\u002FFloor level. Once on the ground, the downward falling stops (",[42,323,324],{},"dy = 0","). But if they hit the jump button, we apply an massive burst of negative upward velocity (",[42,327,328],{},"this.dy = -8",") to launch them!",[54,331,333],{"className":56,"code":332,"language":58,"meta":15,"style":15},"  update() {\n    this.prevState = this.state; \u002F\u002F Track past state to detect animation switching\n    const groundLevel = canvas.height - this.size - 90;\n\n    \u002F\u002F Is the player Y level touching or below Ground floor?\n    const onGround = this.y >= groundLevel;\n\n    if (onGround) {\n      this.y = groundLevel; \u002F\u002F Snap firmly to floor (Anti sink)\n      this.dy = 0; \u002F\u002F Stop gravity fall speed\n\n      \u002F\u002F JUMP trigger from keyboard tracking Map\n      if (this.keys[\"arrowup\"]) {\n        this.dy = -8; \u002F\u002F Instant upward pushing force applied!\n      }\n    } else {\n      this.dy += this.gravity; \u002F\u002F Gravity pulling downward continuously mid-air\n    }\n\n    this.y += this.dy; \u002F\u002F Apply vertical momentum sum to the actual world Y pos!\n\n    \u002F\u002F STATE MACHINE : Intelligently determine what happens based on conditions\n    if (this.y \u003C groundLevel) this.state = \"Jump\";\n    else if (this.keys[\"space\"]) this.state = \"Kick\";\n    else if (this.keys[\"arrowright\"]) this.state = \"MoveForward\";\n    else this.state = \"Idle\";\n\n    \u002F\u002F Auto-reset frame to 0 if animation state changed (e.g., Idle -> Jump)\n    if (this.prevState !== this.state) this.frame = 0;\n  }\n",[42,334,335,340,345,350,354,359,364,368,373,378,383,387,392,397,402,407,412,417,422,426,431,436,442,448,454,460,466,471,477,483],{"__ignoreMap":15},[62,336,337],{"class":64,"line":65},[62,338,339],{},"  update() {\n",[62,341,342],{"class":64,"line":71},[62,343,344],{},"    this.prevState = this.state; \u002F\u002F Track past state to detect animation switching\n",[62,346,347],{"class":64,"line":77},[62,348,349],{},"    const groundLevel = canvas.height - this.size - 90;\n",[62,351,352],{"class":64,"line":83},[62,353,105],{"emptyLinePlaceholder":104},[62,355,356],{"class":64,"line":89},[62,357,358],{},"    \u002F\u002F Is the player Y level touching or below Ground floor?\n",[62,360,361],{"class":64,"line":95},[62,362,363],{},"    const onGround = this.y >= groundLevel;\n",[62,365,366],{"class":64,"line":101},[62,367,105],{"emptyLinePlaceholder":104},[62,369,370],{"class":64,"line":108},[62,371,372],{},"    if (onGround) {\n",[62,374,375],{"class":64,"line":114},[62,376,377],{},"      this.y = groundLevel; \u002F\u002F Snap firmly to floor (Anti sink)\n",[62,379,380],{"class":64,"line":120},[62,381,382],{},"      this.dy = 0; \u002F\u002F Stop gravity fall speed\n",[62,384,385],{"class":64,"line":126},[62,386,105],{"emptyLinePlaceholder":104},[62,388,389],{"class":64,"line":132},[62,390,391],{},"      \u002F\u002F JUMP trigger from keyboard tracking Map\n",[62,393,394],{"class":64,"line":138},[62,395,396],{},"      if (this.keys[\"arrowup\"]) {\n",[62,398,399],{"class":64,"line":144},[62,400,401],{},"        this.dy = -8; \u002F\u002F Instant upward pushing force applied!\n",[62,403,404],{"class":64,"line":150},[62,405,406],{},"      }\n",[62,408,409],{"class":64,"line":156},[62,410,411],{},"    } else {\n",[62,413,414],{"class":64,"line":162},[62,415,416],{},"      this.dy += this.gravity; \u002F\u002F Gravity pulling downward continuously mid-air\n",[62,418,419],{"class":64,"line":168},[62,420,421],{},"    }\n",[62,423,424],{"class":64,"line":299},[62,425,105],{"emptyLinePlaceholder":104},[62,427,428],{"class":64,"line":304},[62,429,430],{},"    this.y += this.dy; \u002F\u002F Apply vertical momentum sum to the actual world Y pos!\n",[62,432,434],{"class":64,"line":433},21,[62,435,105],{"emptyLinePlaceholder":104},[62,437,439],{"class":64,"line":438},22,[62,440,441],{},"    \u002F\u002F STATE MACHINE : Intelligently determine what happens based on conditions\n",[62,443,445],{"class":64,"line":444},23,[62,446,447],{},"    if (this.y \u003C groundLevel) this.state = \"Jump\";\n",[62,449,451],{"class":64,"line":450},24,[62,452,453],{},"    else if (this.keys[\"space\"]) this.state = \"Kick\";\n",[62,455,457],{"class":64,"line":456},25,[62,458,459],{},"    else if (this.keys[\"arrowright\"]) this.state = \"MoveForward\";\n",[62,461,463],{"class":64,"line":462},26,[62,464,465],{},"    else this.state = \"Idle\";\n",[62,467,469],{"class":64,"line":468},27,[62,470,105],{"emptyLinePlaceholder":104},[62,472,474],{"class":64,"line":473},28,[62,475,476],{},"    \u002F\u002F Auto-reset frame to 0 if animation state changed (e.g., Idle -> Jump)\n",[62,478,480],{"class":64,"line":479},29,[62,481,482],{},"    if (this.prevState !== this.state) this.frame = 0;\n",[62,484,486],{"class":64,"line":485},30,[62,487,159],{},[34,489,491],{"id":490},"_4-animating-the-sprite-horizontal-flipping-tricks","4. Animating the Sprite & Horizontal Flipping Tricks",[21,493,494,495,498,499,502],{},"Finally, the ",[42,496,497],{},"animate()"," function controls which image slide from our array is painted. We use a built-in timer limiter logic (",[42,500,501],{},"Date.now()",") to slow down the animation pacing. Otherwise, the frames would change 60 times a second and skip too fast!",[21,504,505,506,509,510,513],{},"And lastly, Remember our Transformation (Origin Flip\u002FScale) lesson? If the player is on the \"Right\" side of the football field team, we want them facing left! So we shift our origin point using ",[42,507,508],{},"ctx.translate"," and mirror their rendering orientation with ",[42,511,512],{},"ctx.scale(-1, 1)","!",[54,515,517],{"className":56,"code":516,"language":58,"meta":15,"style":15},"  animate() {\n    const activeImageArray = this.image[this.state];\n    const now = Date.now();\n    \n    \u002F\u002F Only increment frame every 50ms (Animation Pacing)\n    if (now - this.lastAnimate > 50) {\n      this.frame++;\n      \u002F\u002F Loop back to start if we exceed the frames length\n      if (this.frame >= activeImageArray.length) this.frame = 0; \n      \n      this.lastAnimate = now;\n    }\n\n    \u002F\u002F Flipping trick specifically for Player 2 (Right side)\n    if (this.position === \"right\") {\n      this.ctx.save();\n      \u002F\u002F Move context pivot exactly at character width before scale mirroring!\n      this.ctx.translate(this.x + this.size, this.y);\n      this.ctx.scale(-1, 1); \u002F\u002F Flip Horizontally!\n\n      this.ctx.drawImage(\n        this.image[this.state][this.frame], \u002F\u002F Print the exact sliding image index based on state\n        0, 0, this.size, this.size\n      );\n      this.ctx.restore(); \u002F\u002F Undo flip to secure next canvas painting!\n    } else {\n      \u002F\u002F Normal straightforward draw for Player 1 Left side\n      this.ctx.drawImage(\n        this.image[this.state][this.frame],\n        this.x, this.y, this.size, this.size\n      );\n    }\n  }\n",[42,518,519,524,529,534,538,543,548,553,558,563,568,573,577,581,586,591,596,601,606,611,615,620,625,630,635,640,644,649,653,658,663,668,673],{"__ignoreMap":15},[62,520,521],{"class":64,"line":65},[62,522,523],{},"  animate() {\n",[62,525,526],{"class":64,"line":71},[62,527,528],{},"    const activeImageArray = this.image[this.state];\n",[62,530,531],{"class":64,"line":77},[62,532,533],{},"    const now = Date.now();\n",[62,535,536],{"class":64,"line":83},[62,537,237],{},[62,539,540],{"class":64,"line":89},[62,541,542],{},"    \u002F\u002F Only increment frame every 50ms (Animation Pacing)\n",[62,544,545],{"class":64,"line":95},[62,546,547],{},"    if (now - this.lastAnimate > 50) {\n",[62,549,550],{"class":64,"line":101},[62,551,552],{},"      this.frame++;\n",[62,554,555],{"class":64,"line":108},[62,556,557],{},"      \u002F\u002F Loop back to start if we exceed the frames length\n",[62,559,560],{"class":64,"line":114},[62,561,562],{},"      if (this.frame >= activeImageArray.length) this.frame = 0; \n",[62,564,565],{"class":64,"line":120},[62,566,567],{},"      \n",[62,569,570],{"class":64,"line":126},[62,571,572],{},"      this.lastAnimate = now;\n",[62,574,575],{"class":64,"line":132},[62,576,421],{},[62,578,579],{"class":64,"line":138},[62,580,105],{"emptyLinePlaceholder":104},[62,582,583],{"class":64,"line":144},[62,584,585],{},"    \u002F\u002F Flipping trick specifically for Player 2 (Right side)\n",[62,587,588],{"class":64,"line":150},[62,589,590],{},"    if (this.position === \"right\") {\n",[62,592,593],{"class":64,"line":156},[62,594,595],{},"      this.ctx.save();\n",[62,597,598],{"class":64,"line":162},[62,599,600],{},"      \u002F\u002F Move context pivot exactly at character width before scale mirroring!\n",[62,602,603],{"class":64,"line":168},[62,604,605],{},"      this.ctx.translate(this.x + this.size, this.y);\n",[62,607,608],{"class":64,"line":299},[62,609,610],{},"      this.ctx.scale(-1, 1); \u002F\u002F Flip Horizontally!\n",[62,612,613],{"class":64,"line":304},[62,614,105],{"emptyLinePlaceholder":104},[62,616,617],{"class":64,"line":433},[62,618,619],{},"      this.ctx.drawImage(\n",[62,621,622],{"class":64,"line":438},[62,623,624],{},"        this.image[this.state][this.frame], \u002F\u002F Print the exact sliding image index based on state\n",[62,626,627],{"class":64,"line":444},[62,628,629],{},"        0, 0, this.size, this.size\n",[62,631,632],{"class":64,"line":450},[62,633,634],{},"      );\n",[62,636,637],{"class":64,"line":456},[62,638,639],{},"      this.ctx.restore(); \u002F\u002F Undo flip to secure next canvas painting!\n",[62,641,642],{"class":64,"line":462},[62,643,411],{},[62,645,646],{"class":64,"line":468},[62,647,648],{},"      \u002F\u002F Normal straightforward draw for Player 1 Left side\n",[62,650,651],{"class":64,"line":473},[62,652,619],{},[62,654,655],{"class":64,"line":479},[62,656,657],{},"        this.image[this.state][this.frame],\n",[62,659,660],{"class":64,"line":485},[62,661,662],{},"        this.x, this.y, this.size, this.size\n",[62,664,666],{"class":64,"line":665},31,[62,667,634],{},[62,669,671],{"class":64,"line":670},32,[62,672,421],{},[62,674,676],{"class":64,"line":675},33,[62,677,159],{},[21,679,680],{},"If you can conquer this system, this code pattern acts as the most rock-solid foundational blueprint for virtually every 2D platformer and fighting game code!",[13,682,683,687,703,707,723,808,812,833,926,930,948,962,1113,1117,1130,1144,1302],{"v-slot:id":15},[17,684,686],{"id":685},"html5-canvas-animasi-sprite-dan-fisika-game","HTML5 Canvas: Animasi Sprite dan Fisika Game",[21,688,689,690,693,694,698,699,702],{},"Setelah kita paham Game Loop, manajemen Input, serta teknik memutar Transformasi kanvas, ini saatnya menggunakan kode pamungkas untuk menggabungkan semuanya secara bersamaan. Mari kita bedah struktur fondasi dari ",[25,691,692],{},"Player Class \u002F Kelas Jagoan"," berdasarkan cuplikan kodingan game berstandar produksi nyata (production code). Kita akan menghidupkan hukum ",[695,696,697],"em",{},"Gravitasi"," agar karakter bisa melompat, sekaligus menggunakan sistem transisi ",[695,700,701],{},"Animasi Gambar Sprite"," bersambung!",[34,704,706],{"id":705},"_1-mempersiapkan-koleksi-aset-ekstensif","1. Mempersiapkan Koleksi Aset Ekstensif",[21,708,709,710,713,714,716,717,720,721,52],{},"Lupakan menggambar kotak kaku. Game asli wajib memainkan gambar berderet (mirip film animasi tradisional frame-by-frame). Gambar-gambar tersebut kita muat dalam RAM lalu dan dikelompokkan ke array berdasarkan ",[695,711,712],{},"Status Aktif"," emosinya (seperti kumpulan frame ",[42,715,44],{},", frame saat mundur ",[42,718,719],{},"MoveBackward",", saat melompat ",[42,722,48],{},[54,724,726],{"className":56,"code":725,"language":58,"meta":15,"style":15},"\u002F\u002F Fungsi pembantu perakit objek DOM \u003Cimg \u002F> murni\nfunction loadImg(src) {\n  const img = new Image();\n  img.src = src;\n  return img; \n}\n\n\u002F\u002F Menghimpun array besar gambar dengan ketepatan file numbering\nfunction loadImagePlayer(country) {\n  const ImageState = { Idle: [], Jump: [], MoveForward: [] \u002F* ... *\u002F };\n  \n  \u002F\u002F Contoh: Menyedot 18 potong frame terpisah untuk Animasi Nafas\u002FDiam (Idle)\n  for (let i = 0; i \u003C= 17; i++) {\n    \u002F\u002F Trik Cerdas: String(i).padStart(2, \"0\") otomatis menyulap angka 1 menjadi teks \"01\"\n    ImageState.Idle.push(loadImg(`.\u002FSprites\u002FCharacters\u002F${country}\u002FIdle\u002FIdle_0${String(i).padStart(2, \"0\")}.png`));\n  }\n  return ImageState;\n}\n",[42,727,728,733,737,741,745,750,754,758,763,767,772,776,781,785,790,795,799,804],{"__ignoreMap":15},[62,729,730],{"class":64,"line":65},[62,731,732],{},"\u002F\u002F Fungsi pembantu perakit objek DOM \u003Cimg \u002F> murni\n",[62,734,735],{"class":64,"line":71},[62,736,74],{},[62,738,739],{"class":64,"line":77},[62,740,80],{},[62,742,743],{"class":64,"line":83},[62,744,86],{},[62,746,747],{"class":64,"line":89},[62,748,749],{},"  return img; \n",[62,751,752],{"class":64,"line":95},[62,753,98],{},[62,755,756],{"class":64,"line":101},[62,757,105],{"emptyLinePlaceholder":104},[62,759,760],{"class":64,"line":108},[62,761,762],{},"\u002F\u002F Menghimpun array besar gambar dengan ketepatan file numbering\n",[62,764,765],{"class":64,"line":114},[62,766,117],{},[62,768,769],{"class":64,"line":120},[62,770,771],{},"  const ImageState = { Idle: [], Jump: [], MoveForward: [] \u002F* ... *\u002F };\n",[62,773,774],{"class":64,"line":126},[62,775,129],{},[62,777,778],{"class":64,"line":132},[62,779,780],{},"  \u002F\u002F Contoh: Menyedot 18 potong frame terpisah untuk Animasi Nafas\u002FDiam (Idle)\n",[62,782,783],{"class":64,"line":138},[62,784,141],{},[62,786,787],{"class":64,"line":144},[62,788,789],{},"    \u002F\u002F Trik Cerdas: String(i).padStart(2, \"0\") otomatis menyulap angka 1 menjadi teks \"01\"\n",[62,791,792],{"class":64,"line":150},[62,793,794],{},"    ImageState.Idle.push(loadImg(`.\u002FSprites\u002FCharacters\u002F${country}\u002FIdle\u002FIdle_0${String(i).padStart(2, \"0\")}.png`));\n",[62,796,797],{"class":64,"line":156},[62,798,159],{},[62,800,801],{"class":64,"line":162},[62,802,803],{},"  return ImageState;\n",[62,805,806],{"class":64,"line":168},[62,807,98],{},[34,809,811],{"id":810},"_2-paradigma-kelas-pembungkus-organ-tubuhplayer-class","2. Paradigma Kelas Pembungkus Organ Tubuh(Player Class)",[21,813,814,815,817,818,820,821,823,824,45,826,828,829,45,831,52],{},"Membungkus kode entitas dalam OOP Paradigma JavaScript ",[42,816,180],{}," adalah tata cara teraman di jagad pembuatan game. Si Karakter (Objek) setidaknya harus mengingat perihal letak lokasi keberadaannya (",[42,819,184],{},"), variabel laju luncurannya (",[42,822,188],{},"), pemutar slide frame yang terputar (",[42,825,192],{},[42,827,195],{},"), serta massa hukum gravitasi beban tubuh (",[42,830,199],{},[42,832,202],{},[54,834,836],{"className":56,"code":835,"language":58,"meta":15,"style":15},"class Player {\n  constructor(country, position, ctx) {\n    this.ctx = ctx; \u002F\u002F kanvas layar referensi\n    this.position = position; \u002F\u002F Dari pihak \"left\" atau \"right\"?\n    this.image = loadImagePlayer(country); \n    \n    \u002F\u002F Dimensi dan Hukum Dunia\n    this.size = 150;\n    this.x = position === \"left\" ? 300 : 600; \u002F\u002F Alokasi pijakan start base awal\n    this.y = 0;\n    this.dy = 0; \u002F\u002F Delta Y: Kekuatan Tarik Turun\u002FKeatas (Momentum Vertical)\n    this.gravity = 0.2; \u002F\u002F Tambahan berat badan yang menarik konstan jatuh ke bawah \n\n    \u002F\u002F Ingatan Mesin Status Animasi saat ini (State machine)\n    this.state = \"Idle\"; \n    this.frame = 0; \u002F\u002F Penentu gambar keberapa yg wajib digambar dari array nya\n    this.lastAnimate = Date.now(); \u002F\u002F Stopwatch Delay laju putar Animasi (pacing kontroler)\n  }\n}\n",[42,837,838,842,846,851,856,861,865,870,874,879,884,889,894,898,903,908,913,918,922],{"__ignoreMap":15},[62,839,840],{"class":64,"line":65},[62,841,212],{},[62,843,844],{"class":64,"line":71},[62,845,217],{},[62,847,848],{"class":64,"line":77},[62,849,850],{},"    this.ctx = ctx; \u002F\u002F kanvas layar referensi\n",[62,852,853],{"class":64,"line":83},[62,854,855],{},"    this.position = position; \u002F\u002F Dari pihak \"left\" atau \"right\"?\n",[62,857,858],{"class":64,"line":89},[62,859,860],{},"    this.image = loadImagePlayer(country); \n",[62,862,863],{"class":64,"line":95},[62,864,237],{},[62,866,867],{"class":64,"line":101},[62,868,869],{},"    \u002F\u002F Dimensi dan Hukum Dunia\n",[62,871,872],{"class":64,"line":108},[62,873,247],{},[62,875,876],{"class":64,"line":114},[62,877,878],{},"    this.x = position === \"left\" ? 300 : 600; \u002F\u002F Alokasi pijakan start base awal\n",[62,880,881],{"class":64,"line":120},[62,882,883],{},"    this.y = 0;\n",[62,885,886],{"class":64,"line":126},[62,887,888],{},"    this.dy = 0; \u002F\u002F Delta Y: Kekuatan Tarik Turun\u002FKeatas (Momentum Vertical)\n",[62,890,891],{"class":64,"line":132},[62,892,893],{},"    this.gravity = 0.2; \u002F\u002F Tambahan berat badan yang menarik konstan jatuh ke bawah \n",[62,895,896],{"class":64,"line":138},[62,897,105],{"emptyLinePlaceholder":104},[62,899,900],{"class":64,"line":144},[62,901,902],{},"    \u002F\u002F Ingatan Mesin Status Animasi saat ini (State machine)\n",[62,904,905],{"class":64,"line":150},[62,906,907],{},"    this.state = \"Idle\"; \n",[62,909,910],{"class":64,"line":156},[62,911,912],{},"    this.frame = 0; \u002F\u002F Penentu gambar keberapa yg wajib digambar dari array nya\n",[62,914,915],{"class":64,"line":162},[62,916,917],{},"    this.lastAnimate = Date.now(); \u002F\u002F Stopwatch Delay laju putar Animasi (pacing kontroler)\n",[62,919,920],{"class":64,"line":168},[62,921,159],{},[62,923,924],{"class":64,"line":299},[62,925,98],{},[34,927,929],{"id":928},"_3-penerapan-ilmu-fisika-gravitasi-lemparan-bebas","3. Penerapan Ilmu Fisika: Gravitasi & Lemparan Bebas",[21,931,932,933,935,936,939,940,943,944,947],{},"Konsep simulasi gravitasi sungguh brilian dan tak terduga! Di dalam ",[42,934,316],{},", variabel momentum tubuh vertikal ",[42,937,938],{},"this.dy"," terus menerus ditambah nilainya setiap ",[695,941,942],{},"Frame"," oleh variabel beban konstan ",[42,945,946],{},"this.gravity",". Oleh karenanya badannya senantiasa terdorongan turun semakin jatuh!",[21,949,950,951,954,955,957,958,961],{},"Kecuali ia menabrak daratan (",[42,952,953],{},"groundLevel","). Sewaktu kakinya persis menyentuh tanah, perhitungan Gravitasi seketika ter-reset hangus menjadi nol (",[42,956,324],{},"). Tapii apabila ia membentur tombol Jump saat berpijak di dasar itu? Kita langsung jejalkan suntikan angka lemparan Ke atas bertolak keras (- Minus) pada dy (",[42,959,960],{},"dy = -8","), membuatnya sejenak melambung melawan gravitasi layaknya rudal roket terbang sesaat menyentuh angkasa!",[54,963,965],{"className":56,"code":964,"language":58,"meta":15,"style":15},"  update() {\n    this.prevState = this.state; \u002F\u002F Recall ingatan di masa lalu memonitor pergantian aksi layar\n    \n    const groundLevel = canvas.height - this.size - 90; \u002F\u002F Kalkulasikan ketinggian batas tapak Lantai mutlak\n\n    \u002F\u002F Cek apakah sol sepatu `Y` nya tembus ke garis Tanah bumi?\n    const onGround = this.y >= groundLevel;\n\n    if (onGround) {\n      this.y = groundLevel; \u002F\u002F Paksa Posisi menempel sejajar di aspal (Anti-Glitch tembus lantai layar mati masuk neraka)\n      this.dy = 0; \u002F\u002F Matikan Beban Daya Tumbuk Jatuhnya\n\n      \u002F\u002F Trigger MELEDAKKAN Lompatan\n      if (this.keys[\"arrowup\"]) {\n        this.dy = -8; \u002F\u002F Tembakkan Rudal daya roket berakselerasi Ke-atas instan!\n      }\n    } else {\n      \u002F\u002F Jika nyatanya masih Melayang Udara, izinkan rantai hukum grafitasi mencekiknya memeras kekuatannya untuk jatuh ke tanah terus menerus tiap sekian MS \n      this.dy += this.gravity; \n    }\n\n    \u002F\u002F Akhirnya suntikan Total Akumulasi kalkulasi Vertikal (dy), mendarat dan mencemari Angka Titik Pijakan Lokasi Aslinya (y)\n    this.y += this.dy; \n\n    \u002F\u002F STATE MACHINE : Mesin Pendiktean Pergolakan Penggantian Aksi\n    if (this.y \u003C groundLevel) this.state = \"Jump\";\n    else if (this.keys[\"space\"]) this.state = \"Kick\";\n    else if (this.keys[\"arrowright\"]) this.state = \"MoveForward\";\n    else this.state = \"Idle\";\n\n    \u002F\u002F Mekanisme Keamanan: Reset Auto-rewind penggulungan slide video kembali ke file img indeks '0' ketika pergantian antar Gerak Tiba (spti diam tau2 lompat)\n    if (this.prevState !== this.state) this.frame = 0; \n  }\n",[42,966,967,971,976,980,985,989,994,998,1002,1006,1011,1016,1020,1025,1029,1034,1038,1042,1047,1052,1056,1060,1065,1070,1074,1079,1083,1087,1091,1095,1099,1104,1109],{"__ignoreMap":15},[62,968,969],{"class":64,"line":65},[62,970,339],{},[62,972,973],{"class":64,"line":71},[62,974,975],{},"    this.prevState = this.state; \u002F\u002F Recall ingatan di masa lalu memonitor pergantian aksi layar\n",[62,977,978],{"class":64,"line":77},[62,979,237],{},[62,981,982],{"class":64,"line":83},[62,983,984],{},"    const groundLevel = canvas.height - this.size - 90; \u002F\u002F Kalkulasikan ketinggian batas tapak Lantai mutlak\n",[62,986,987],{"class":64,"line":89},[62,988,105],{"emptyLinePlaceholder":104},[62,990,991],{"class":64,"line":95},[62,992,993],{},"    \u002F\u002F Cek apakah sol sepatu `Y` nya tembus ke garis Tanah bumi?\n",[62,995,996],{"class":64,"line":101},[62,997,363],{},[62,999,1000],{"class":64,"line":108},[62,1001,105],{"emptyLinePlaceholder":104},[62,1003,1004],{"class":64,"line":114},[62,1005,372],{},[62,1007,1008],{"class":64,"line":120},[62,1009,1010],{},"      this.y = groundLevel; \u002F\u002F Paksa Posisi menempel sejajar di aspal (Anti-Glitch tembus lantai layar mati masuk neraka)\n",[62,1012,1013],{"class":64,"line":126},[62,1014,1015],{},"      this.dy = 0; \u002F\u002F Matikan Beban Daya Tumbuk Jatuhnya\n",[62,1017,1018],{"class":64,"line":132},[62,1019,105],{"emptyLinePlaceholder":104},[62,1021,1022],{"class":64,"line":138},[62,1023,1024],{},"      \u002F\u002F Trigger MELEDAKKAN Lompatan\n",[62,1026,1027],{"class":64,"line":144},[62,1028,396],{},[62,1030,1031],{"class":64,"line":150},[62,1032,1033],{},"        this.dy = -8; \u002F\u002F Tembakkan Rudal daya roket berakselerasi Ke-atas instan!\n",[62,1035,1036],{"class":64,"line":156},[62,1037,406],{},[62,1039,1040],{"class":64,"line":162},[62,1041,411],{},[62,1043,1044],{"class":64,"line":168},[62,1045,1046],{},"      \u002F\u002F Jika nyatanya masih Melayang Udara, izinkan rantai hukum grafitasi mencekiknya memeras kekuatannya untuk jatuh ke tanah terus menerus tiap sekian MS \n",[62,1048,1049],{"class":64,"line":299},[62,1050,1051],{},"      this.dy += this.gravity; \n",[62,1053,1054],{"class":64,"line":304},[62,1055,421],{},[62,1057,1058],{"class":64,"line":433},[62,1059,105],{"emptyLinePlaceholder":104},[62,1061,1062],{"class":64,"line":438},[62,1063,1064],{},"    \u002F\u002F Akhirnya suntikan Total Akumulasi kalkulasi Vertikal (dy), mendarat dan mencemari Angka Titik Pijakan Lokasi Aslinya (y)\n",[62,1066,1067],{"class":64,"line":444},[62,1068,1069],{},"    this.y += this.dy; \n",[62,1071,1072],{"class":64,"line":450},[62,1073,105],{"emptyLinePlaceholder":104},[62,1075,1076],{"class":64,"line":456},[62,1077,1078],{},"    \u002F\u002F STATE MACHINE : Mesin Pendiktean Pergolakan Penggantian Aksi\n",[62,1080,1081],{"class":64,"line":462},[62,1082,447],{},[62,1084,1085],{"class":64,"line":468},[62,1086,453],{},[62,1088,1089],{"class":64,"line":473},[62,1090,459],{},[62,1092,1093],{"class":64,"line":479},[62,1094,465],{},[62,1096,1097],{"class":64,"line":485},[62,1098,105],{"emptyLinePlaceholder":104},[62,1100,1101],{"class":64,"line":665},[62,1102,1103],{},"    \u002F\u002F Mekanisme Keamanan: Reset Auto-rewind penggulungan slide video kembali ke file img indeks '0' ketika pergantian antar Gerak Tiba (spti diam tau2 lompat)\n",[62,1105,1106],{"class":64,"line":670},[62,1107,1108],{},"    if (this.prevState !== this.state) this.frame = 0; \n",[62,1110,1111],{"class":64,"line":675},[62,1112,159],{},[34,1114,1116],{"id":1115},"_4-render-animasi-frame-beruntun-pemantulan-cermin-otomatis-kaca-tim","4. Render Animasi Frame Beruntun & Pemantulan Cermin Otomatis Kaca Tim",[21,1118,1119,1120,1123,1124,1126,1127,1129],{},"Keahlian terakhir metode ",[695,1121,1122],{},"core"," tersembunyi Sang Jagoan berada di ",[42,1125,497],{},". Skrip ini menggunakan logika cerdas pembatas Timer rentang pias Waktu (",[42,1128,501],{},"). Laju delay perulangan antar gambar dimanipulasi dengan batas angka \"lebih dari 50 Milikdetik\" supaya framenya tertahan ganti secara rasional. Bila limit ini tidak di rem, maka 18 frame tendangan animasimu akan habis secepat panah dalam wujud \"Seper Enam-Puluh\" (1\u002F60) detik di layar monitor! Manusia tidak akan menyadarinya.",[21,1131,1132,1133,1136,1137,1140,1141,1143],{},"Dan Terakhir, mari gunakan materi ",[695,1134,1135],{},"Transformasi Flip (Minus Scala 1)"," yang baru saja diajarkan untuk Player pihak sebelah Tim Kanan (",[42,1138,1139],{},"position === \"right\"","). Daripada kerepotan melukis manual karakter dengan gambar pose menghindar mengarah sebaliknya hadap kiri, kita cukup menyuruh Program melakukan Rotasi pencerminan dimensi koordinat ",[42,1142,512],{}," sesaat sebelum nge-print (drawImage) ke hadap Tim Sang Pemain Kiri Musuhnya.",[54,1145,1147],{"className":56,"code":1146,"language":58,"meta":15,"style":15},"  animate() {\n    \u002F\u002F Intip Array Koleksi Foto yang aktif saat Status Animainya di call ini\n    const KumpulanGambarAnimasiAktif = this.image[this.state];\n    const jamSkrg = Date.now();\n    \n    \u002F\u002F Gembok Stopwatch: Frame Tayang slide gambar selanjutnya HANYA tergeser apabila Waktu sudah terdiam berjaraknya melewati 50ms \n    if (jamSkrg - this.lastAnimate > 50) {\n      this.frame++; \u002F\u002F Geser penunjuk slider ke Next Nomor gambar \n      \u002F\u002F Gulung pemutar klise gambar jadi 0, persis sama saat Film animasinya habis tak tersisa \n      if (this.frame >= KumpulanGambarAnimasiAktif.length) this.frame = 0; \n      \n      this.lastAnimate = jamSkrg;\n    }\n\n    \u002F\u002F Trik Pembalikan Kaca Cermin Ekslusif Hanya untuk Pemain Posisi Markas \"Kanan\" Lapangan\n    if (this.position === \"right\") {\n      this.ctx.save();\n      \u002F\u002F Pusat Geser pindah tembus menjarah ke sayap depan titik x Character supaya orientasi cermin tdk ngawur\n      this.ctx.translate(this.x + this.size, this.y); \n      this.ctx.scale(-1, 1); \u002F\u002F Flip Putaran Bayangan Cermin Horizontal 180 Drajat Mutlak!\n\n      this.ctx.drawImage(\n        this.image[this.state][this.frame], \u002F\u002F Pilih selambar spesifik file gambar index persis sesuai penempatannya.\n        0, 0, this.size, this.size\n      );\n      this.ctx.restore(); \u002F\u002F Amankan ruang Kanvas kembali. Lepas bayangannya\n    } else {\n      \u002F\u002F Menggambar Normal Biasa untuk kubu Pemain di sisi Kiri Lapangan yang emang fitrahnya aslinya tergambar menghadap arah kanan\n      this.ctx.drawImage(\n        this.image[this.state][this.frame],\n        this.x, this.y, this.size, this.size\n      );\n    }\n  }\n",[42,1148,1149,1153,1158,1163,1168,1172,1177,1182,1187,1192,1197,1201,1206,1210,1214,1219,1223,1227,1232,1237,1242,1246,1250,1255,1259,1263,1268,1272,1277,1281,1285,1289,1293,1297],{"__ignoreMap":15},[62,1150,1151],{"class":64,"line":65},[62,1152,523],{},[62,1154,1155],{"class":64,"line":71},[62,1156,1157],{},"    \u002F\u002F Intip Array Koleksi Foto yang aktif saat Status Animainya di call ini\n",[62,1159,1160],{"class":64,"line":77},[62,1161,1162],{},"    const KumpulanGambarAnimasiAktif = this.image[this.state];\n",[62,1164,1165],{"class":64,"line":83},[62,1166,1167],{},"    const jamSkrg = Date.now();\n",[62,1169,1170],{"class":64,"line":89},[62,1171,237],{},[62,1173,1174],{"class":64,"line":95},[62,1175,1176],{},"    \u002F\u002F Gembok Stopwatch: Frame Tayang slide gambar selanjutnya HANYA tergeser apabila Waktu sudah terdiam berjaraknya melewati 50ms \n",[62,1178,1179],{"class":64,"line":101},[62,1180,1181],{},"    if (jamSkrg - this.lastAnimate > 50) {\n",[62,1183,1184],{"class":64,"line":108},[62,1185,1186],{},"      this.frame++; \u002F\u002F Geser penunjuk slider ke Next Nomor gambar \n",[62,1188,1189],{"class":64,"line":114},[62,1190,1191],{},"      \u002F\u002F Gulung pemutar klise gambar jadi 0, persis sama saat Film animasinya habis tak tersisa \n",[62,1193,1194],{"class":64,"line":120},[62,1195,1196],{},"      if (this.frame >= KumpulanGambarAnimasiAktif.length) this.frame = 0; \n",[62,1198,1199],{"class":64,"line":126},[62,1200,567],{},[62,1202,1203],{"class":64,"line":132},[62,1204,1205],{},"      this.lastAnimate = jamSkrg;\n",[62,1207,1208],{"class":64,"line":138},[62,1209,421],{},[62,1211,1212],{"class":64,"line":144},[62,1213,105],{"emptyLinePlaceholder":104},[62,1215,1216],{"class":64,"line":150},[62,1217,1218],{},"    \u002F\u002F Trik Pembalikan Kaca Cermin Ekslusif Hanya untuk Pemain Posisi Markas \"Kanan\" Lapangan\n",[62,1220,1221],{"class":64,"line":156},[62,1222,590],{},[62,1224,1225],{"class":64,"line":162},[62,1226,595],{},[62,1228,1229],{"class":64,"line":168},[62,1230,1231],{},"      \u002F\u002F Pusat Geser pindah tembus menjarah ke sayap depan titik x Character supaya orientasi cermin tdk ngawur\n",[62,1233,1234],{"class":64,"line":299},[62,1235,1236],{},"      this.ctx.translate(this.x + this.size, this.y); \n",[62,1238,1239],{"class":64,"line":304},[62,1240,1241],{},"      this.ctx.scale(-1, 1); \u002F\u002F Flip Putaran Bayangan Cermin Horizontal 180 Drajat Mutlak!\n",[62,1243,1244],{"class":64,"line":433},[62,1245,105],{"emptyLinePlaceholder":104},[62,1247,1248],{"class":64,"line":438},[62,1249,619],{},[62,1251,1252],{"class":64,"line":444},[62,1253,1254],{},"        this.image[this.state][this.frame], \u002F\u002F Pilih selambar spesifik file gambar index persis sesuai penempatannya.\n",[62,1256,1257],{"class":64,"line":450},[62,1258,629],{},[62,1260,1261],{"class":64,"line":456},[62,1262,634],{},[62,1264,1265],{"class":64,"line":462},[62,1266,1267],{},"      this.ctx.restore(); \u002F\u002F Amankan ruang Kanvas kembali. Lepas bayangannya\n",[62,1269,1270],{"class":64,"line":468},[62,1271,411],{},[62,1273,1274],{"class":64,"line":473},[62,1275,1276],{},"      \u002F\u002F Menggambar Normal Biasa untuk kubu Pemain di sisi Kiri Lapangan yang emang fitrahnya aslinya tergambar menghadap arah kanan\n",[62,1278,1279],{"class":64,"line":479},[62,1280,619],{},[62,1282,1283],{"class":64,"line":485},[62,1284,657],{},[62,1286,1287],{"class":64,"line":665},[62,1288,662],{},[62,1290,1291],{"class":64,"line":670},[62,1292,634],{},[62,1294,1295],{"class":64,"line":675},[62,1296,421],{},[62,1298,1300],{"class":64,"line":1299},34,[62,1301,159],{},[21,1303,1304],{},"Bila dinaluri sedalam mungkin, arsitektur pattern di dalam file OOP kelas ini adalah Masterpiece mutlak dalam pembentukan hampir di seluruh kodingan dari game model tarung pertarungan (Fight\u002FBrawler), mapun Loncat-Loncatan Platform klasik Mario, Megaman yang ada belahan game manapun di muka dunia! Terus asah logika insting pengkodean keren hebat tersebut!",[1306,1307,1308],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":15,"searchDepth":71,"depth":71,"links":1310},[],"md",{"title_en":5,"title_id":686,"description_en":1313,"description_id":1314,"date":1315,"readingTime":1316},"Learn to build a complete Player Class with sprite frame animation, gravity physics, and state machines based on real game code.","Pelajari cara membangun Player Class lengkap dengan animasi frame sprite, fisika gravitasi, dan mesin state berdasarkan kode game nyata.","2026-03-25","10 min read","\u002Fblog\u002Fcanvas-sprite-physics",{"title":5,"description":15},"blog\u002Fcanvas-sprite-physics","75r73uZAexZp5wd-mZ_97ssvqhb3azzeebhC9xI3rBo",1783751391566]