<?xml version='1.0' encoding='UTF-8'?><rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/" xmlns:blogger="http://schemas.google.com/blogger/2008" xmlns:georss="http://www.georss.org/georss" xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr="http://purl.org/syndication/thread/1.0" version="2.0"><channel><atom:id>tag:blogger.com,1999:blog-7685167172351671465</atom:id><lastBuildDate>Tue, 08 Sep 2026 19:44:49 +0000</lastBuildDate><category>Tutoriales</category><category>HTML5</category><category>Phaser</category><category>Games</category><category>canvas</category><category>en-games</category><category>JavaScript</category><category>lang-en</category><category>HTML5 Game</category><category>Herramientas</category><category>Phaser 4</category><category>juegos con html5</category><category>juegos con phaser</category><category>Experimentos</category><category>game UI</category><category>naves</category><category>videojuegos</category><category>Space</category><category>Three.js</category><category>WebGL</category><category>animaciones</category><category>eventos</category><category>game VFX</category><category>js13k</category><category>mouse</category><category>programar</category><category>Canvas 2D game physics</category><category>Canvas accretion disk</category><category>Canvas particle animation</category><category>Canvas particle explosion</category><category>HTML5 Breakout game</category><category>HTML5 Canvas JavaScript</category><category>HTML5 Canvas animation</category><category>HTML5 game menu button</category><category>HTML5 game particle effect</category><category>HTML5 game tutorial&#xa;Labels: lang-en</category><category>HTML5 platformer game</category><category>INVADERS</category><category>Invader</category><category>JavaScript 2D platformer physics</category><category>JavaScript black hole animation</category><category>JavaScript black hole effect</category><category>JavaScript card battle UI Canvas</category><category>JavaScript game FX</category><category>JavaScript game UI effect</category><category>JavaScript gravity game</category><category>JavaScript gravity simulation</category><category>JavaScript magnetic button game menu</category><category>JavaScript neon trail effect HTML5 game</category><category>JavaScript particle effect HTML5 game</category><category>JavaScript top-down shooter Canvas</category><category>Juegos</category><category>Migración</category><category>Phaser 3</category><category>Phaser 4 Breakout game</category><category>Phaser 4 brick breaker</category><category>Phaser 4 tutorial</category><category>Recursos</category><category>Responsive</category><category>ShoeBox</category><category>Sinuous</category><category>Three.js particle effect HTML5 game WebGL</category><category>TypeScript</category><category>Vite</category><category>Wamp</category><category>agujero negro JavaScript</category><category>animación Canvas JavaScript</category><category>arcade game</category><category>black hole animation JavaScript</category><category>browser game JavaScript</category><category>browser platformer tutorial&#xa;Labels: lang-en</category><category>camara</category><category>como hacer</category><category>como hacer jeugos</category><category>como hacer musica</category><category>competicion</category><category>concursos</category><category>desarrollar</category><category>disco de acreción Canvas</category><category>efecto agujero negro JavaScript</category><category>estados</category><category>feliz año</category><category>fgl</category><category>fisica</category><category>fisica en phaser</category><category>flash</category><category>framework</category><category>framework phaser</category><category>frameworks</category><category>game hit effect JavaScript</category><category>game menu JavaScript</category><category>ganar</category><category>ganar dinero con fgl</category><category>graficos</category><category>gratis</category><category>grupos</category><category>hacer dinero con juegos</category><category>hacer juegos con phaser</category><category>js13k-</category><category>json</category><category>juego con Phaser</category><category>juego en html5</category><category>juegos en html5</category><category>juicy game button effect&#xa;Labels: lang-en</category><category>lang-es</category><category>libro</category><category>little strong</category><category>magnetic hover game</category><category>minijuego con phaser</category><category>movil</category><category>particle system browser game&#xa;lang-en</category><category>particles</category><category>particulas</category><category>partículas Canvas JavaScript</category><category>plataforma</category><category>plataformas</category><category>platformer jump JavaScript</category><category>plugins para phaser</category><category>programacion</category><category>programas para hacer musica</category><category>reciclar</category><category>simulación gravedad JavaScript</category><category>spritsheets</category><category>teclado</category><category>texturas</category><category>tween</category><title>Haciendo juegos con Html5</title><description>Blog donde aprenderas a hacer juegos en html5 con phaser y mucho mas</description><link>http://juegosenhtml5.blogspot.com/</link><managingEditor>noreply@blogger.com (a)</managingEditor><generator>Blogger</generator><openSearch:totalResults>61</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-1790277400087372926</guid><pubDate>Fri, 04 Sep 2026 13:20:25 +0000</pubDate><atom:updated>2026-09-04T08:20:25.685-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">canvas</category><category domain="http://www.blogger.com/atom/ns#">en-games</category><category domain="http://www.blogger.com/atom/ns#">game UI</category><category domain="http://www.blogger.com/atom/ns#">Games</category><category domain="http://www.blogger.com/atom/ns#">HTML5 Game</category><category domain="http://www.blogger.com/atom/ns#">JavaScript</category><category domain="http://www.blogger.com/atom/ns#">lang-en</category><title>Canvas Game HUD — Animated Health Bar, Mana &amp; XP in JavaScript</title><description>&lt;!--
BLOGGER POST - EN

Title / H1:
Canvas Game HUD - Animated Health Bar, Mana &amp; XP in JavaScript

Suggested slug:
canvas-game-hud-health-mana-bars

Search description:
Build a game HUD with animated health bar, mana bar, XP progress bar, skill cooldowns, and a mini-map using JavaScript Canvas.

Primary keyword:
Canvas game HUD health bar JavaScript

Secondary keywords:
HTML5 game HUD
JavaScript health bar animation
game UI Canvas
game status bar
mana bar HTML5 game

Labels:
lang-en, en-games, Games, JavaScript, Canvas, game UI, HTML5 Game

Series:
HTML5 Games from Scratch

Social title:
Build a Game HUD with Animated Bars in JavaScript Canvas

Thumbnail text:
HP: ████
--&gt;


&lt;article
  class=&quot;jh-exp&quot;
  data-project=&quot;canvas-game-hud-health-mana-bars&quot;
  data-primary-keyword=&quot;Canvas game HUD health bar JavaScript&quot;
&gt;

  &lt;p class=&quot;jh-article-lead&quot;&gt;
    &lt;strong&gt;Every RPG, action game, and shooter needs a HUD.&lt;/strong&gt;
    This demo builds the complete set: an animated health bar,
    mana bar, XP progress bar, skill cooldown slots, damage feedback,
    level-up effects, and a mini-map, entirely with Canvas 2D.
    You can adapt this system to almost any HTML5 game.
  &lt;/p&gt;


  &lt;div class=&quot;jh-article-note&quot;&gt;

    &lt;div class=&quot;jh-article-note-icon&quot;&gt;INFO&lt;/div&gt;

    &lt;div&gt;

      &lt;strong&gt;What you will build&lt;/strong&gt;

      &lt;p&gt;
        A Canvas HUD that automatically cycles through damage,
        healing, XP gain, and level-up states.
        Bar values animate smoothly toward their targets instead
        of jumping instantly.
      &lt;/p&gt;

    &lt;/div&gt;

  &lt;/div&gt;



  &lt;!-- =========================================================
       LIVE DEMO
  ========================================================== --&gt;

  &lt;section
    class=&quot;jh-exp-stage&quot;
    id=&quot;demo-canvas_game_hud_health_mana_bars&quot;
  &gt;

    &lt;div class=&quot;jh-exp-stage-head&quot;&gt;

      &lt;div&gt;

        &lt;span class=&quot;jh-exp-kicker&quot;&gt;
          LIVE GAME UI DEMO
        &lt;/span&gt;

        &lt;h2&gt;
          Canvas Game HUD - Health, Mana &amp; XP
        &lt;/h2&gt;

      &lt;/div&gt;

      &lt;span class=&quot;jh-exp-hint&quot;&gt;
        Watch the HUD animate through damage, healing, XP gain,
        and level-up states
      &lt;/span&gt;

    &lt;/div&gt;


    &lt;div class=&quot;jh-exp-canvas-wrap&quot;&gt;

      &lt;canvas
        class=&quot;jh-exp-canvas&quot;
        aria-label=&quot;Canvas game HUD demo&quot;
      &gt;&lt;/canvas&gt;

    &lt;/div&gt;

  &lt;/section&gt;



  &lt;!-- =========================================================
       GAME HUD SCRIPT
  ========================================================== --&gt;

  &lt;script&gt;
  (function () {

    &#39;use strict&#39;;


    /* =========================================================
       GET CANVAS
    ========================================================== */

    const root = document.getElementById(
      &#39;demo-canvas_game_hud_health_mana_bars&#39;
    );

    if (!root) return;


    const canvas = root.querySelector(&#39;canvas&#39;);

    if (!canvas) return;


    const ctx = canvas.getContext(&#39;2d&#39;);

    if (!ctx) return;



    /* =========================================================
       CANVAS RESIZE
    ========================================================== */

    let dpr = 1;


    function resizeCanvas() {

      const rect = canvas.getBoundingClientRect();

      dpr = Math.min(
        window.devicePixelRatio || 1,
        2
      );


      const width = Math.max(
        1,
        Math.round(rect.width * dpr)
      );


      const height = Math.max(
        1,
        Math.round(rect.height * dpr)
      );


      if (
        canvas.width !== width ||
        canvas.height !== height
      ) {

        canvas.width = width;
        canvas.height = height;

      }


      ctx.setTransform(
        dpr,
        0,
        0,
        dpr,
        0,
        0
      );

    }


    resizeCanvas();


    if (&#39;ResizeObserver&#39; in window) {

      const observer = new ResizeObserver(
        resizeCanvas
      );

      observer.observe(canvas);

    } else {

      window.addEventListener(
        &#39;resize&#39;,
        resizeCanvas
      );

    }



    /* =========================================================
       GAME STATE
    ========================================================== */

    const state = {

      hp: 1,
      hpTarget: 1,

      mp: 0.72,
      mpTarget: 0.72,

      xp: 0.32,
      xpTarget: 0.32,

      level: 1,

      flash: 0,

      levelBurst: 0

    };



    /* =========================================================
       SKILLS

       cd:
       remaining cooldown time in seconds

       maxCd:
       maximum cooldown time
    ========================================================== */

    const skills = [

      {
        cd: 0,
        maxCd: 3
      },

      {
        cd: 1.6,
        maxCd: 4
      },

      {
        cd: 2.8,
        maxCd: 5
      },

      {
        cd: 0.7,
        maxCd: 3
      }

    ];



    /* =========================================================
       DEMO PHASES
    ========================================================== */

    const PHASE_TIME = 2.8;


    const phaseNames = [

      &#39;DAMAGE&#39;,

      &#39;HEALING&#39;,

      &#39;XP GAIN&#39;,

      &#39;LEVEL UP&#39;

    ];


    let phase = 0;

    let phaseTimer = 0;

    let lastTime = performance.now();



    /* =========================================================
       HELPERS
    ========================================================== */

    function clamp(
      value,
      min,
      max
    ) {

      return Math.max(
        min,
        Math.min(max, value)
      );

    }



    function smoothTo(
      current,
      target,
      speed,
      dt
    ) {

      const amount =
        1 - Math.exp(-speed * dt);


      return current +
        (target - current) *
        amount;

    }



    /* =========================================================
       CUSTOM ROUNDED RECTANGLE

       We do not depend on ctx.roundRect().
       This improves compatibility.
    ========================================================== */

    function roundedRectPath(
      x,
      y,
      width,
      height,
      radius
    ) {

      if (
        width &lt;= 0 ||
        height &lt;= 0
      ) {

        return;

      }


      radius = Math.min(
        radius,
        width / 2,
        height / 2
      );


      ctx.beginPath();


      ctx.moveTo(
        x + radius,
        y
      );


      ctx.lineTo(
        x + width - radius,
        y
      );


      ctx.quadraticCurveTo(
        x + width,
        y,
        x + width,
        y + radius
      );


      ctx.lineTo(
        x + width,
        y + height - radius
      );


      ctx.quadraticCurveTo(
        x + width,
        y + height,
        x + width - radius,
        y + height
      );


      ctx.lineTo(
        x + radius,
        y + height
      );


      ctx.quadraticCurveTo(
        x,
        y + height,
        x,
        y + height - radius
      );


      ctx.lineTo(
        x,
        y + radius
      );


      ctx.quadraticCurveTo(
        x,
        y,
        x + radius,
        y
      );


      ctx.closePath();

    }



    /* =========================================================
       PHASE EVENTS

       IMPORTANT:
       These actions happen only once when a phase starts.
    ========================================================== */

    function startPhase(
      nextPhase
    ) {

      phase = nextPhase;


      /* DAMAGE */

      if (phase === 0) {

        state.hpTarget = 0.28;

        state.flash = 1;


        skills[0].cd =
          skills[0].maxCd;

      }



      /* HEALING */

      if (phase === 1) {

        state.hpTarget = 0.82;

      }



      /* XP + MANA */

      if (phase === 2) {

        state.mpTarget = 0.96;

        state.xpTarget = 1;


        skills[2].cd =
          skills[2].maxCd;

      }



      /* LEVEL UP */

      if (phase === 3) {

        state.level += 1;


        state.xp = 0;

        state.xpTarget = 0.14;


        state.levelBurst = 1;

      }

    }



    /* =========================================================
       BAR
    ========================================================== */

    function drawBar(
      x,
      y,
      width,
      height,
      value,
      fillColor,
      backgroundColor,
      label
    ) {

      value = clamp(
        value,
        0,
        1
      );


      ctx.save();



      /* Background */

      ctx.fillStyle =
        backgroundColor;


      roundedRectPath(
        x,
        y,
        width,
        height,
        height / 2
      );


      ctx.fill();



      /* Filled area */

      const fillWidth =
        width * value;


      if (fillWidth &gt; 0) {

        ctx.fillStyle =
          fillColor;


        roundedRectPath(
          x,
          y,
          fillWidth,
          height,
          Math.min(
            height / 2,
            fillWidth / 2
          )
        );


        ctx.fill();

      }



      /* Highlight */

      if (fillWidth &gt; 8) {

        ctx.fillStyle =
          &#39;rgba(255,255,255,0.16)&#39;;


        roundedRectPath(
          x + 2,
          y + 2,
          Math.max(
            0,
            fillWidth - 4
          ),
          Math.max(
            2,
            height * 0.30
          ),
          height / 4
        );


        ctx.fill();

      }



      /* Text */

      ctx.font =
        &#39;600 11px Arial, Helvetica, sans-serif&#39;;


      ctx.textBaseline =
        &#39;middle&#39;;


      ctx.fillStyle =
        &#39;#ffffff&#39;;



      /* Label */

      ctx.textAlign =
        &#39;left&#39;;


      ctx.fillText(
        label,
        x + 8,
        y + height / 2
      );



      /* Value */

      ctx.textAlign =
        &#39;right&#39;;


      const valueText =
        Math.round(value * 100) +
        &#39; / 100&#39;;


      ctx.fillText(
        valueText,
        x + width - 8,
        y + height / 2
      );


      ctx.restore();

    }



    /* =========================================================
       MINIMAP
    ========================================================== */

    function drawMinimap(
      x,
      y,
      width,
      height
    ) {

      ctx.save();



      /* Background */

      ctx.fillStyle =
        &#39;rgba(2,10,16,0.92)&#39;;


      ctx.fillRect(
        x,
        y,
        width,
        height
      );



      /* Border */

      ctx.strokeStyle =
        &#39;#20c9e8&#39;;


      ctx.lineWidth =
        1.5;


      ctx.strokeRect(
        x + 0.75,
        y + 0.75,
        width - 1.5,
        height - 1.5
      );



      /* Enemy dots */

      const enemies = [

        [0.70, 0.30],

        [0.20, 0.78],

        [0.88, 0.60],

        [0.40, 0.47],

        [0.14, 0.22]

      ];


      ctx.fillStyle =
        &#39;#ff4242&#39;;


      for (
        let i = 0;
        i &lt; enemies.length;
        i++
      ) {

        const enemy =
          enemies[i];


        const ex =
          enemy[0];


        const ey =
          enemy[1];


        ctx.fillRect(
          x + ex * width - 2,
          y + ey * height - 2,
          4,
          4
        );

      }



      /* Player detection circle */

      ctx.fillStyle =
        &#39;rgba(32,201,232,0.18)&#39;;


      ctx.beginPath();


      ctx.arc(
        x + width / 2,
        y + height / 2,
        16,
        0,
        Math.PI * 2
      );


      ctx.fill();



      /* Player */

      ctx.fillStyle =
        &#39;#20c9e8&#39;;


      ctx.fillRect(
        x + width / 2 - 3,
        y + height / 2 - 3,
        6,
        6
      );



      /* Header */

      ctx.font =
        &#39;600 9px Arial, Helvetica, sans-serif&#39;;


      ctx.textBaseline =
        &#39;top&#39;;



      ctx.textAlign =
        &#39;left&#39;;


      ctx.fillStyle =
        &#39;rgba(255,255,255,0.78)&#39;;


      ctx.fillText(
        &#39;MAP&#39;,
        x + 6,
        y + 5
      );



      ctx.textAlign =
        &#39;right&#39;;


      ctx.fillStyle =
        &#39;#20c9e8&#39;;


      ctx.fillText(
        &#39;LV &#39; + state.level,
        x + width - 6,
        y + 5
      );


      ctx.restore();

    }



    /* =========================================================
       VECTOR SKILL ICONS

       IMPORTANT:
       No emoji.
       No HTML entities.
       No special Unicode characters.

       Everything is drawn directly with Canvas.
    ========================================================== */

    function drawSkillIcon(
      index,
      x,
      y,
      size
    ) {

      ctx.save();


      ctx.translate(
        x,
        y
      );


      ctx.strokeStyle =
        &#39;#ffffff&#39;;


      ctx.fillStyle =
        &#39;#ffffff&#39;;


      ctx.lineWidth =
        2;


      ctx.lineCap =
        &#39;round&#39;;


      ctx.lineJoin =
        &#39;round&#39;;



      /* -----------------------------------------
         SKILL 1
         Crossed blades
      ----------------------------------------- */

      if (index === 0) {

        ctx.beginPath();


        ctx.moveTo(
          size * 0.27,
          size * 0.25
        );


        ctx.lineTo(
          size * 0.72,
          size * 0.72
        );


        ctx.moveTo(
          size * 0.73,
          size * 0.25
        );


        ctx.lineTo(
          size * 0.28,
          size * 0.72
        );


        ctx.stroke();



        ctx.beginPath();


        ctx.moveTo(
          size * 0.20,
          size * 0.67
        );


        ctx.lineTo(
          size * 0.32,
          size * 0.79
        );


        ctx.moveTo(
          size * 0.80,
          size * 0.67
        );


        ctx.lineTo(
          size * 0.68,
          size * 0.79
        );


        ctx.stroke();

      }



      /* -----------------------------------------
         SKILL 2
         Flame
      ----------------------------------------- */

      if (index === 1) {

        ctx.beginPath();


        ctx.moveTo(
          size * 0.50,
          size * 0.16
        );


        ctx.bezierCurveTo(
          size * 0.63,
          size * 0.36,
          size * 0.80,
          size * 0.48,
          size * 0.72,
          size * 0.67
        );


        ctx.bezierCurveTo(
          size * 0.67,
          size * 0.81,
          size * 0.53,
          size * 0.86,
          size * 0.42,
          size * 0.79
        );


        ctx.bezierCurveTo(
          size * 0.22,
          size * 0.66,
          size * 0.29,
          size * 0.45,
          size * 0.42,
          size * 0.34
        );


        ctx.bezierCurveTo(
          size * 0.45,
          size * 0.30,
          size * 0.48,
          size * 0.23,
          size * 0.50,
          size * 0.16
        );


        ctx.closePath();


        ctx.fill();



        ctx.fillStyle =
          &#39;#101820&#39;;


        ctx.beginPath();


        ctx.moveTo(
          size * 0.50,
          size * 0.46
        );


        ctx.bezierCurveTo(
          size * 0.61,
          size * 0.58,
          size * 0.60,
          size * 0.72,
          size * 0.49,
          size * 0.75
        );


        ctx.bezierCurveTo(
          size * 0.38,
          size * 0.70,
          size * 0.39,
          size * 0.57,
          size * 0.50,
          size * 0.46
        );


        ctx.closePath();


        ctx.fill();

      }



      /* -----------------------------------------
         SKILL 3
         Lightning
      ----------------------------------------- */

      if (index === 2) {

        ctx.beginPath();


        ctx.moveTo(
          size * 0.57,
          size * 0.14
        );


        ctx.lineTo(
          size * 0.30,
          size * 0.52
        );


        ctx.lineTo(
          size * 0.47,
          size * 0.52
        );


        ctx.lineTo(
          size * 0.38,
          size * 0.86
        );


        ctx.lineTo(
          size * 0.72,
          size * 0.42
        );


        ctx.lineTo(
          size * 0.54,
          size * 0.42
        );


        ctx.closePath();


        ctx.fill();

      }



      /* -----------------------------------------
         SKILL 4
         Shield
      ----------------------------------------- */

      if (index === 3) {

        ctx.beginPath();


        ctx.moveTo(
          size * 0.50,
          size * 0.16
        );


        ctx.lineTo(
          size * 0.75,
          size * 0.27
        );


        ctx.lineTo(
          size * 0.70,
          size * 0.62
        );


        ctx.quadraticCurveTo(
          size * 0.65,
          size * 0.76,
          size * 0.50,
          size * 0.84
        );


        ctx.quadraticCurveTo(
          size * 0.35,
          size * 0.76,
          size * 0.30,
          size * 0.62
        );


        ctx.lineTo(
          size * 0.25,
          size * 0.27
        );


        ctx.closePath();


        ctx.stroke();



        ctx.beginPath();


        ctx.moveTo(
          size * 0.50,
          size * 0.27
        );


        ctx.lineTo(
          size * 0.50,
          size * 0.70
        );


        ctx.stroke();

      }


      ctx.restore();

    }



    /* =========================================================
       SKILL SLOT
    ========================================================== */

    function drawSkillSlot(
      x,
      y,
      size,
      skill,
      index
    ) {

      ctx.save();



      /* Background */

      ctx.fillStyle =
        &#39;rgba(2,8,13,0.92)&#39;;


      ctx.fillRect(
        x,
        y,
        size,
        size
      );



      /* Border */

      ctx.strokeStyle =
        skill.cd &gt; 0
          ? &#39;rgba(120,150,165,0.80)&#39;
          : &#39;#20c9e8&#39;;


      ctx.lineWidth =
        1.5;


      ctx.strokeRect(
        x + 0.75,
        y + 0.75,
        size - 1.5,
        size - 1.5
      );



      /* Icon */

      ctx.globalAlpha =
        skill.cd &gt; 0
          ? 0.45
          : 1;


      drawSkillIcon(
        index,
        x,
        y,
        size
      );


      ctx.globalAlpha = 1;



      /* Cooldown overlay */

      if (skill.cd &gt; 0) {

        const ratio =
          clamp(
            skill.cd /
            skill.maxCd,
            0,
            1
          );


        ctx.fillStyle =
          &#39;rgba(0,0,0,0.60)&#39;;


        ctx.fillRect(
          x,
          y +
            size *
            (1 - ratio),
          size,
          size * ratio
        );



        /* Cooldown text */

        ctx.font =
          &#39;700 11px Arial, Helvetica, sans-serif&#39;;


        ctx.textAlign =
          &#39;center&#39;;


        ctx.textBaseline =
          &#39;middle&#39;;


        ctx.fillStyle =
          &#39;#ffffff&#39;;


        ctx.fillText(
          Math.ceil(skill.cd) +
          &#39;s&#39;,
          x + size / 2,
          y + size / 2
        );

      }



      /* Skill number */

      ctx.font =
        &#39;600 8px Arial, Helvetica, sans-serif&#39;;


      ctx.textAlign =
        &#39;left&#39;;


      ctx.textBaseline =
        &#39;top&#39;;


      ctx.fillStyle =
        &#39;rgba(255,255,255,0.65)&#39;;


      ctx.fillText(
        String(index + 1),
        x + 4,
        y + 3
      );


      ctx.restore();

    }



    /* =========================================================
       BACKGROUND
    ========================================================== */

    function drawBackground(
      width,
      height
    ) {

      /* Dark base */

      ctx.fillStyle =
        &#39;#06141d&#39;;


      ctx.fillRect(
        0,
        0,
        width,
        height
      );



      /* Game world placeholder */

      ctx.fillStyle =
        &#39;rgba(8,45,62,0.82)&#39;;


      ctx.fillRect(
        0,
        0,
        width,
        height
      );



      /* Grid */

      ctx.strokeStyle =
        &#39;rgba(40,160,190,0.10)&#39;;


      ctx.lineWidth = 1;



      for (
        let x = 0;
        x &lt;= width;
        x += 40
      ) {

        ctx.beginPath();


        ctx.moveTo(
          x,
          0
        );


        ctx.lineTo(
          x,
          height
        );


        ctx.stroke();

      }



      for (
        let y = 0;
        y &lt;= height;
        y += 40
      ) {

        ctx.beginPath();


        ctx.moveTo(
          0,
          y
        );


        ctx.lineTo(
          width,
          y
        );


        ctx.stroke();

      }

    }



    /* =========================================================
       DAMAGE BACKGROUND FLASH

       Drawn before the HUD so text remains readable.
    ========================================================== */

    function drawDamageFlash(
      width,
      height
    ) {

      if (state.flash &lt;= 0) {

        return;

      }


      ctx.fillStyle =
        &#39;rgba(255,30,30,&#39; +
        (state.flash * 0.16) +
        &#39;)&#39;;


      ctx.fillRect(
        0,
        0,
        width,
        height
      );

    }



    /* =========================================================
       DAMAGE BORDER

       The border can safely be drawn above the HUD.
    ========================================================== */

    function drawDamageBorder(
      width,
      height
    ) {

      if (state.flash &lt;= 0) {

        return;

      }


      ctx.strokeStyle =
        &#39;rgba(255,40,40,&#39; +
        (state.flash * 0.82) +
        &#39;)&#39;;


      ctx.lineWidth =
        5;


      ctx.strokeRect(
        2.5,
        2.5,
        width - 5,
        height - 5
      );

    }



    /* =========================================================
       LEVEL UP EFFECT
    ========================================================== */

    function drawLevelBurst(
      width,
      height
    ) {

      if (
        state.levelBurst &lt;= 0
      ) {

        return;

      }


      const progress =
        1 -
        state.levelBurst;


      const radius =
        20 +
        progress *
        width *
        0.35;



      /* Circle */

      ctx.strokeStyle =
        &#39;rgba(220,90,255,&#39; +
        state.levelBurst +
        &#39;)&#39;;


      ctx.lineWidth =
        4;


      ctx.beginPath();


      ctx.arc(
        width / 2,
        height / 2,
        radius,
        0,
        Math.PI * 2
      );


      ctx.stroke();



      /* Text */

      ctx.font =
        &#39;700 22px Arial, Helvetica, sans-serif&#39;;


      ctx.textAlign =
        &#39;center&#39;;


      ctx.textBaseline =
        &#39;middle&#39;;


      ctx.fillStyle =
        &#39;rgba(245,210,255,&#39; +
        state.levelBurst +
        &#39;)&#39;;


      ctx.fillText(
        &#39;LEVEL UP! &#39; +
        state.level,
        width / 2,
        height / 2
      );

    }



    /* =========================================================
       DEMO STATUS TEXT
    ========================================================== */

    function drawStatus(
      height,
      padding
    ) {

      ctx.font =
        &#39;600 10px Arial, Helvetica, sans-serif&#39;;


      ctx.textAlign =
        &#39;left&#39;;


      ctx.textBaseline =
        &#39;bottom&#39;;


      ctx.fillStyle =
        &#39;rgba(255,255,255,0.65)&#39;;


      ctx.fillText(
        &#39;DEMO: &#39; +
        phaseNames[phase],
        padding,
        height - padding
      );

    }



    /* =========================================================
       INITIAL PHASE
    ========================================================== */

    startPhase(0);



    /* =========================================================
       ANIMATION LOOP
    ========================================================== */

    function frame(now) {

      const width =
        canvas.clientWidth;


      const height =
        canvas.clientHeight;



      if (
        !width ||
        !height
      ) {

        requestAnimationFrame(
          frame
        );

        return;

      }



      /* Delta time */

      let dt =
        (now - lastTime) /
        1000;


      lastTime = now;



      /* Prevent huge jumps after switching tabs */

      dt = Math.min(
        dt,
        0.05
      );



      /* =====================================================
         PHASE TIMER
      ===================================================== */

      phaseTimer += dt;


      if (
        phaseTimer &gt;= PHASE_TIME
      ) {

        phaseTimer -=
          PHASE_TIME;


        const nextPhase =
          (phase + 1) %
          phaseNames.length;


        startPhase(
          nextPhase
        );

      }



      /* =====================================================
         SMOOTH VALUES
      ===================================================== */

      state.hp = smoothTo(
        state.hp,
        state.hpTarget,
        7.5,
        dt
      );


      state.mp = smoothTo(
        state.mp,
        state.mpTarget,
        5.5,
        dt
      );


      state.xp = smoothTo(
        state.xp,
        state.xpTarget,
        5,
        dt
      );



      /* Slow mana consumption */

      state.mpTarget =
        Math.max(
          0.15,
          state.mpTarget -
          dt * 0.025
        );



      /* Damage flash decay */

      state.flash =
        Math.max(
          0,
          state.flash -
          dt * 2.4
        );



      /* Level burst decay */

      state.levelBurst =
        Math.max(
          0,
          state.levelBurst -
          dt * 1.2
        );



      /* =====================================================
         COOLDOWNS
      ===================================================== */

      for (
        let i = 0;
        i &lt; skills.length;
        i++
      ) {

        skills[i].cd =
          Math.max(
            0,
            skills[i].cd -
            dt
          );

      }



      /* =====================================================
         DRAW BACKGROUND
      ===================================================== */

      drawBackground(
        width,
        height
      );



      /* Damage tint goes behind HUD */

      drawDamageFlash(
        width,
        height
      );



      /* =====================================================
         RESPONSIVE HUD LAYOUT
      ===================================================== */

      const padding = 16;


      const barHeight = 16;

      const barGap = 7;



      const mapWidth =
        width &lt; 600
          ? 78
          : 96;


      const mapHeight =
        width &lt; 600
          ? 64
          : 76;


      const mapX =
        width -
        padding -
        mapWidth;


      const mapY =
        padding;



      /*
       * Bars stop before the minimap.
       * This prevents HP, MP, XP text overlap.
       */

      const availableBarWidth =
        mapX -
        padding -
        14;


      const barWidth =
        Math.max(
          100,
          availableBarWidth
        );



      /* =====================================================
         HEALTH COLOR
      ===================================================== */

      let hpColor;


      if (state.hp &lt; 0.25) {

        hpColor =
          &#39;#ff3030&#39;;

      } else if (
        state.hp &lt; 0.50
      ) {

        hpColor =
          &#39;#ff9028&#39;;

      } else {

        hpColor =
          &#39;#35dd68&#39;;

      }



      /* =====================================================
         DRAW BARS
      ===================================================== */

      drawBar(
        padding,
        padding,
        barWidth,
        barHeight,
        state.hp,
        hpColor,
        &#39;rgba(0,0,0,0.55)&#39;,
        &#39;HP&#39;
      );


      drawBar(
        padding,
        padding +
          barHeight +
          barGap,
        barWidth,
        barHeight,
        state.mp,
        &#39;#4388ff&#39;,
        &#39;rgba(0,0,0,0.55)&#39;,
        &#39;MP&#39;
      );


      drawBar(
        padding,
        padding +
          (
            barHeight +
            barGap
          ) *
          2,
        barWidth,
        barHeight,
        state.xp,
        &#39;#d94de4&#39;,
        &#39;rgba(0,0,0,0.55)&#39;,
        &#39;XP&#39;
      );



      /* =====================================================
         DRAW MINIMAP
      ===================================================== */

      drawMinimap(
        mapX,
        mapY,
        mapWidth,
        mapHeight
      );



      /* =====================================================
         SKILL SLOTS
      ===================================================== */

      const slotSize =
        width &lt; 500
          ? 32
          : 40;


      const slotGap =
        width &lt; 500
          ? 6
          : 8;


      const totalSkillsWidth =
        skills.length *
        slotSize +
        (
          skills.length - 1
        ) *
        slotGap;


      const slotStartX =
        (
          width -
          totalSkillsWidth
        ) /
        2;


      const slotY =
        height -
        slotSize -
        padding;



      for (
        let i = 0;
        i &lt; skills.length;
        i++
      ) {

        drawSkillSlot(
          slotStartX +
            i *
            (
              slotSize +
              slotGap
            ),
          slotY,
          slotSize,
          skills[i],
          i
        );

      }



      /* =====================================================
         EFFECTS ABOVE HUD
      ===================================================== */

      drawDamageBorder(
        width,
        height
      );


      drawLevelBurst(
        width,
        height
      );



      /* =====================================================
         STATUS
      ===================================================== */

      drawStatus(
        height,
        padding
      );



      requestAnimationFrame(
        frame
      );

    }



    requestAnimationFrame(
      frame
    );


  })();
  &lt;/script&gt;



  &lt;!-- =========================================================
       ARTICLE CONTENT
  ========================================================== --&gt;

  &lt;h2&gt;
    How to build animated game bars in Canvas
  &lt;/h2&gt;


  &lt;p&gt;
    Never set a HUD bar directly to the new game value if you
    want smooth animation. Instead, keep a displayed value and
    a target value. Each frame, move the displayed value toward
    the target.
  &lt;/p&gt;


  &lt;p&gt;
    A simple version looks like this:
    &lt;code&gt;
      displayed += (target - displayed) * 0.05
    &lt;/code&gt;.
    This turns instant number changes into smooth visual movement
    without requiring a tweening library.
  &lt;/p&gt;


  &lt;p&gt;
    The demo above uses a frame-rate-independent version of the
    same idea. This means the animation remains consistent even
    when the game runs at different frame rates.
  &lt;/p&gt;


  &lt;p&gt;
    Health color can also change dynamically. In this example,
    the bar is green at high health, orange at medium health, and
    red when health becomes critical.
  &lt;/p&gt;


  &lt;p&gt;
    Damage feedback uses a red screen tint plus a red border.
    The important detail is that the full-screen tint is drawn
    before the HUD. This keeps HP, MP, XP, cooldown numbers, and
    the mini-map readable during the damage effect.
  &lt;/p&gt;



  &lt;!-- =========================================================
       CODE EXAMPLE
  ========================================================== --&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;

    &lt;div class=&quot;jh-exp-code-head&quot;&gt;

      &lt;span&gt;
        hud.js
      &lt;/span&gt;

      &lt;span&gt;
        KEY IDEA
      &lt;/span&gt;

    &lt;/div&gt;


    &lt;pre&gt;&lt;code data-code-source&gt;// Smooth a displayed HUD value toward its target
function smoothTo(current, target, speed, dt) {
  const amount = 1 - Math.exp(-speed * dt);
  return current + (target - current) * amount;
}

displayedHP = smoothTo(
  displayedHP,
  targetHP,
  7.5,
  deltaTime
);

// Change health color by percentage
const barColor =
  displayedHP &gt; 0.50
    ? &#39;#35dd68&#39;
    : displayedHP &gt; 0.25
    ? &#39;#ff9028&#39;
    : &#39;#ff3030&#39;;&lt;/code&gt;&lt;/pre&gt;

  &lt;/div&gt;


  &lt;p class=&quot;jh-exp-caption&quot;&gt;
    Smooth interpolation plus health thresholds creates a
    responsive animated HUD without a tweening library.
  &lt;/p&gt;



  &lt;!-- =========================================================
       COOLDOWNS
  ========================================================== --&gt;

  &lt;h2&gt;
    Skill cooldown slots
  &lt;/h2&gt;


  &lt;p&gt;
    Each skill stores its remaining cooldown in seconds.
    Every frame, subtract delta time until the value reaches zero.
  &lt;/p&gt;


  &lt;p&gt;
    The HUD converts the remaining cooldown into a value between
    zero and one and uses that percentage to draw a dark overlay
    over the skill slot.
  &lt;/p&gt;


  &lt;p&gt;
    The remaining number of seconds is drawn in the center of the
    slot with:
    &lt;code&gt;
      Math.ceil(skill.cd)
    &lt;/code&gt;.
  &lt;/p&gt;


  &lt;p&gt;
    The icons in this demo are drawn directly with Canvas paths.
    They do not use emoji characters or HTML entities. This avoids
    font and character encoding problems that can occur when code
    is inserted into Blogger.
  &lt;/p&gt;



  &lt;!-- =========================================================
       GAME INTEGRATION
  ========================================================== --&gt;

  &lt;div class=&quot;jh-article-current&quot;&gt;

    &lt;strong&gt;
      Add this HUD to your game
    &lt;/strong&gt;


    &lt;p&gt;
      In a real game, the HUD should be drawn after the game world.
      Render your map, player, enemies, particles, and effects first,
      then render the HUD on top.
    &lt;/p&gt;


    &lt;p&gt;
      A typical game loop would look like this:
    &lt;/p&gt;


    &lt;pre&gt;&lt;code&gt;function frame() {

  updateGame();

  drawWorld();

  drawPlayer();

  drawEnemies();

  drawParticles();

  drawHUD();

  requestAnimationFrame(frame);

}&lt;/code&gt;&lt;/pre&gt;


    &lt;p&gt;
      Keep the game&#39;s real state separate from the HUD&#39;s displayed
      state. For example, the game may instantly change player HP
      from 100 to 40 while the HUD smoothly animates its displayed
      value from 100 toward 40.
    &lt;/p&gt;

  &lt;/div&gt;


&lt;/article&gt;</description><link>http://juegosenhtml5.blogspot.com/2026/09/canvas-game-hud-health-mana-bars.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-3179407043853164549</guid><pubDate>Fri, 04 Sep 2026 12:36:07 +0000</pubDate><atom:updated>2026-09-04T07:36:07.222-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">canvas</category><category domain="http://www.blogger.com/atom/ns#">en-games</category><category domain="http://www.blogger.com/atom/ns#">game UI</category><category domain="http://www.blogger.com/atom/ns#">Games</category><category domain="http://www.blogger.com/atom/ns#">HTML5 Game</category><category domain="http://www.blogger.com/atom/ns#">JavaScript</category><category domain="http://www.blogger.com/atom/ns#">lang-en</category><title>JavaScript Game Loading Screen — Animated Liquid Fill &amp; Progress Canvas</title><description>&lt;!--
BLOGGER POST — EN
Title / H1: JavaScript Game Loading Screen — Animated Liquid Fill &amp; Progress Canvas
Suggested slug: javascript-game-loading-screen-canvas
Search description: Build a game loading screen in JavaScript Canvas — animated liquid fill progress bar, particle splash effects, and smooth transitions. Copy into any HTML5 game.
Primary keyword: JavaScript game loading screen Canvas
Secondary keywords: HTML5 game loading screen, Canvas loading animation, game progress bar JavaScript, browser game loading, animated loading screen game
Labels: lang-en, en-games, Games, JavaScript, Canvas, game UI, HTML5 Game
Series: HTML5 Games from Scratch
Social title: Game Loading Screen in JavaScript — Liquid Fill Animation Canvas
Thumbnail text: LOADING
--&gt;
&lt;article class=&quot;jh-exp&quot; data-project=&quot;javascript-game-loading-screen-canvas&quot; data-primary-keyword=&quot;JavaScript game loading screen Canvas&quot;&gt;
  &lt;p class=&quot;jh-article-lead&quot;&gt;&lt;strong&gt;First impressions matter.&lt;/strong&gt; This demo builds an eye-catching game loading screen — a liquid-fill progress bar with particle splashes, a pulsing logo, shimmer particles, and a smooth &quot;Loading complete&quot; transition. Zero libraries, pure Canvas 2D. Drop this into your HTML5 game as the first screen players see.&lt;/p&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;
    &lt;div&gt;
      &lt;strong&gt;What you will build&lt;/strong&gt;
      &lt;p&gt;A Canvas loading screen with a sinusoidal liquid wave inside a progress bar container, particles that burst from the fill edge, a percentage counter, and a game title that pulses with a glow. The progress fills at a configurable rate and loops for demo purposes — in a real game, tie it to your asset loading callbacks.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;section class=&quot;jh-exp-stage&quot; id=&quot;demo-javascript_game_loading_screen_canvas&quot;&gt;
  &lt;div class=&quot;jh-exp-stage-head&quot;&gt;
    &lt;div&gt;&lt;span class=&quot;jh-exp-kicker&quot;&gt;LIVE GAME UI DEMO&lt;/span&gt;&lt;h2&gt;Game Loading Screen — JavaScript Canvas&lt;/h2&gt;&lt;/div&gt;
    &lt;span class=&quot;jh-exp-hint&quot;&gt;Watch the liquid fill animation loop&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-exp-canvas-wrap&quot;&gt;
    &lt;canvas class=&quot;jh-exp-canvas&quot; aria-label=&quot;Game loading screen Canvas demo&quot;&gt;&lt;/canvas&gt;
  &lt;/div&gt;
&lt;/section&gt;
&lt;script&gt;
(function(){
 const root=document.getElementById(&#39;demo-javascript_game_loading_screen_canvas&#39;);
 const canvas=root.querySelector(&#39;canvas&#39;);
 const ctx=canvas.getContext(&#39;2d&#39;);
 function resize(){const r=canvas.getBoundingClientRect(),dpr=Math.min(window.devicePixelRatio||1,2);canvas.width=Math.floor(r.width*dpr);canvas.height=Math.floor(r.height*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);}
 resize();new ResizeObserver(resize).observe(canvas);
 const W=()=&gt;canvas.clientWidth,H=()=&gt;canvas.clientHeight;

 let progress=0,waveT=0,particles=[],t=0,complete=false,completeT=0;

 function spawnParticles(x,y,count){
  for(let i=0;i&lt;count;i++){
   const a=Math.random()*Math.PI*2,s=Math.random()*3+1;
   particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-2,life:40,color:`hsl(${180+Math.random()*40},80%,60%)`});
  }
 }

 function drawLiquidBar(bx,by,bw,bh,prog,wt){
  // clipping region
  ctx.save();ctx.beginPath();ctx.roundRect(bx,by,bw,bh,bh/2);ctx.clip();
  // bg
  ctx.fillStyle=&#39;rgba(0,0,0,0.5)&#39;;ctx.fillRect(bx,by,bw,bh);
  // wave fill
  const fillW=bw*prog;
  const waveH=bh*0.35;
  ctx.beginPath();ctx.moveTo(bx,by+bh);
  for(let x2=bx;x2&lt;=bx+fillW+8;x2+=4){
   const y2=by+bh-bh*prog+Math.sin((x2-bx)*0.06+wt)*waveH*0.5+Math.sin((x2-bx)*0.1+wt*1.5)*waveH*0.3;
   ctx.lineTo(x2,y2);
  }
  ctx.lineTo(bx+fillW,by+bh);ctx.lineTo(bx,by+bh);ctx.closePath();
  const grad=ctx.createLinearGradient(bx,by,bx,by+bh);
  grad.addColorStop(0,&#39;rgba(40,220,255,0.9)&#39;);grad.addColorStop(1,&#39;rgba(8,100,200,0.9)&#39;);
  ctx.fillStyle=grad;ctx.fill();
  // shine
  ctx.fillStyle=&#39;rgba(255,255,255,0.15)&#39;;
  ctx.fillRect(bx,by+2,fillW,bh*0.3);
  ctx.restore();
  // border
  ctx.strokeStyle=&#39;rgba(8,200,228,0.7)&#39;;ctx.lineWidth=2;
  ctx.beginPath();ctx.roundRect(bx,by,bw,bh,bh/2);ctx.stroke();
 }

 function frame(){
  const w=W(),h=H();t+=0.016;waveT+=0.07;

  if(!complete){
   const prevProg=progress;
   progress=Math.min(1,progress+0.005);
   // spawn particles at fill edge
   if(Math.floor(prevProg*100)!==Math.floor(progress*100)&amp;&amp;Math.random()&lt;0.4){
    const barW=w*0.65,barX=(w-barW)/2;
    spawnParticles(barX+barW*progress,h*0.62,3);
   }
   if(progress&gt;=1){complete=true;}
  } else {
   completeT+=0.02;
   if(completeT&gt;2){progress=0;complete=false;completeT=0;particles=[];}
  }

  // update particles
  for(let i=particles.length-1;i&gt;=0;i--){
   const p=particles[i];p.x+=p.vx;p.y+=p.vy;p.vy+=0.15;p.life--;
   if(p.life&lt;=0)particles.splice(i,1);
  }

  // background
  const bg=ctx.createRadialGradient(w/2,h/2,50,w/2,h/2,w*0.8);
  bg.addColorStop(0,&#39;#061828&#39;);bg.addColorStop(1,&#39;#020810&#39;);
  ctx.fillStyle=bg;ctx.fillRect(0,0,w,h);

  // star field
  ctx.fillStyle=&#39;rgba(255,255,255,0.3)&#39;;
  for(let i=0;i&lt;50;i++){const sx=(i*137)%w,sy=(i*97)%h;ctx.fillRect(sx,sy,1,1);}

  // spinning decorative rings
  for(let ring=0;ring&lt;3;ring++){
   ctx.save();ctx.translate(w/2,h*0.32);ctx.rotate(t*(0.3+ring*0.2)*(ring%2?1:-1));
   ctx.strokeStyle=`rgba(8,200,228,${0.1-ring*0.025})`;ctx.lineWidth=1;
   for(let i=0;i&lt;8;i++){
    ctx.beginPath();ctx.arc(60+ring*25,0,4+ring*2,0,Math.PI*2);ctx.stroke();
    ctx.rotate(Math.PI/4);
   }
   ctx.restore();
  }

  // game title / logo
  ctx.save();ctx.translate(w/2,h*0.32);
  const pulse=1+Math.sin(t*2)*0.015;ctx.scale(pulse,pulse);
  ctx.shadowBlur=complete?20+Math.sin(completeT*6)*10:12;
  ctx.shadowColor=complete?&#39;#44ffcc&#39;:&#39;#08c8e4&#39;;
  ctx.fillStyle=complete?&#39;#44ffcc&#39;:&#39;#08c8e4&#39;;ctx.font=&#39;bold 32px monospace&#39;;
  ctx.textAlign=&#39;center&#39;;ctx.fillText(&#39;ASTRAL QUEST&#39;,0,10);
  ctx.shadowBlur=0;ctx.fillStyle=&#39;rgba(255,255,255,0.5)&#39;;ctx.font=&#39;11px monospace&#39;;
  ctx.fillText(&#39;AN HTML5 ADVENTURE&#39;,0,30);
  ctx.restore();ctx.textAlign=&#39;left&#39;;

  // loading bar
  const barW=w*0.65,barH=18,barX=(w-barW)/2,barY=h*0.6;
  drawLiquidBar(barX,barY,barW,barH,progress,waveT);

  // percentage
  ctx.fillStyle=&#39;rgba(8,200,228,0.85)&#39;;ctx.font=&#39;bold 13px monospace&#39;;ctx.textAlign=&#39;center&#39;;
  ctx.fillText(Math.round(progress*100)+&#39;%&#39;,w/2,barY+barH+20);
  ctx.fillStyle=&#39;rgba(255,255,255,0.35)&#39;;ctx.font=&#39;10px monospace&#39;;
  if(!complete)ctx.fillText(&#39;LOADING GAME ASSETS...&#39;,w/2,barY+barH+36);
  else ctx.fillText(&#39;READY! ENTERING WORLD...&#39;,w/2,barY+barH+36);
  ctx.textAlign=&#39;left&#39;;

  // particles
  for(const p of particles){
   ctx.globalAlpha=p.life/40;ctx.fillStyle=p.color;ctx.fillRect(p.x-2,p.y-2,4,4);
  }
  ctx.globalAlpha=1;

  // complete flash
  if(complete&amp;&amp;completeT&lt;0.3){
   ctx.fillStyle=`rgba(40,220,255,${(0.3-completeT)/0.3*0.2})`;ctx.fillRect(0,0,w,h);
  }

  requestAnimationFrame(frame);
 }
 requestAnimationFrame(frame);
})();
&lt;/script&gt;

  &lt;h2&gt;How the liquid fill effect works in Canvas&lt;/h2&gt;
  &lt;p&gt;The liquid fill is a sine wave path clipped to the progress bar rectangle. Draw a wave by looping through X positions and computing &lt;code&gt;y = barTop + Math.sin(x * frequency + time) * amplitude&lt;/code&gt;. Close the path back to the bottom-left corner and fill it. The advancing fill width is just &lt;code&gt;fillWidth = barWidth * progress&lt;/code&gt; — the wave is drawn up to that point. Together, the animated wave inside a clipping region creates the liquid illusion.&lt;/p&gt;
  &lt;p&gt;Clip using &lt;code&gt;ctx.save(); ctx.beginPath(); ctx.roundRect(...); ctx.clip();&lt;/code&gt; before drawing the wave. Everything drawn inside the clip region is automatically masked to the bar shape. Call &lt;code&gt;ctx.restore()&lt;/code&gt; to remove the clip for subsequent drawing.&lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;
    &lt;div class=&quot;jh-exp-code-head&quot;&gt;&lt;span&gt;loading.js&lt;/span&gt;&lt;span&gt;KEY IDEA&lt;/span&gt;&lt;/div&gt;
    &lt;pre&gt;&lt;code data-code-source&gt;// Clip to progress bar shape
ctx.save();
ctx.beginPath();
ctx.roundRect(barX, barY, barWidth, barHeight, radius);
ctx.clip();

// Draw animated liquid wave
ctx.beginPath();
ctx.moveTo(barX, barY + barHeight);
for (let x = barX; x &amp;lt;= barX + fillWidth; x += 4) {
  const y = barY + barHeight - barHeight * progress
          + Math.sin(x * 0.06 + waveTime) * waveAmp;
  ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fill();
ctx.restore(); // remove clip&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
  &lt;p class=&quot;jh-exp-caption&quot;&gt;Clip + sine wave = liquid fill. Advance &lt;code&gt;waveTime&lt;/code&gt; each frame and the liquid appears to slosh.&lt;/p&gt;

  &lt;h2&gt;Connecting to real asset loading&lt;/h2&gt;
  &lt;p&gt;In a real game, tie &lt;code&gt;progress&lt;/code&gt; to asset loading callbacks. Using Phaser, listen to the scene&#39;s &lt;code&gt;progress&lt;/code&gt; event: &lt;code&gt;this.load.on(&#39;progress&#39;, value =&gt; { loadingProgress = value; })&lt;/code&gt;. Using plain JavaScript, count loaded images with an &lt;code&gt;onload&lt;/code&gt; counter: &lt;code&gt;progress = loadedCount / totalAssets&lt;/code&gt;. The visual loading screen is completely decoupled from asset loading — it just reads the number.&lt;/p&gt;

  &lt;div class=&quot;jh-article-current&quot;&gt;
    &lt;strong&gt;Integrate into your HTML5 game&lt;/strong&gt;
    &lt;p&gt;Show the loading screen as the first state in your game&#39;s state machine. Start asset loading immediately, update &lt;code&gt;progress&lt;/code&gt; as callbacks fire, and transition to the title screen when &lt;code&gt;progress &gt;= 1&lt;/code&gt;. Add a minimum display time (0.5–1 second) so the screen doesn&#39;t flash instantly on fast connections — players need a moment to read the title.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;
</description><link>http://juegosenhtml5.blogspot.com/2026/09/javascript-game-loading-screen-canvas.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-648678849833086423</guid><pubDate>Thu, 03 Sep 2026 18:30:00 +0000</pubDate><atom:updated>2026-09-03T13:30:00.237-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">en-games</category><category domain="http://www.blogger.com/atom/ns#">game UI</category><category domain="http://www.blogger.com/atom/ns#">Games</category><category domain="http://www.blogger.com/atom/ns#">HTML5 Game</category><category domain="http://www.blogger.com/atom/ns#">lang-en</category><category domain="http://www.blogger.com/atom/ns#">Three.js</category><category domain="http://www.blogger.com/atom/ns#">WebGL</category><title>Three.js 3D Game Level Selection — Draggable 3D Carousel with Momentum</title><description>&lt;!--
BLOGGER POST — EN
Title / H1: Three.js 3D Game Level Selection — Draggable 3D Carousel with Momentum
Suggested slug: threejs-3d-carousel-drag
Search description: Build a Three.js 3D level selection carousel for games — cylindrical drag with momentum, snap-to-panel, and depth focus. Perfect for character or stage select screens.
Primary keyword: Three.js 3D level selection game carousel
Secondary keywords: Three.js game menu, 3D level select carousel, HTML5 game character select, Three.js drag carousel tutorial, WebGL game UI carousel
Labels: lang-en, en-games, Games, Three.js, WebGL, game UI, HTML5 Game
Series: HTML5 Games from Scratch
Social title: 3D Game Level Selection Carousel in Three.js — Drag + Momentum
Thumbnail text: SELECT!
--&gt;
&lt;article class=&quot;jh-exp&quot; data-project=&quot;threejs-3d-carousel-drag&quot; data-primary-keyword=&quot;Three.js 3D level selection game carousel&quot;&gt;
  &lt;p class=&quot;jh-article-lead&quot;&gt;&lt;strong&gt;This is how game character select and level select screens should feel.&lt;/strong&gt; Drag left or right and 8 panels orbit around a virtual cylinder — the panel in front depth-focuses, drag-release applies momentum that coasts to a stop, and the nearest panel snaps to front. Zero carousel libraries, just Three.js, trigonometry, and pointer events. Build your game&#39;s selection screen in an afternoon.&lt;/p&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;
    &lt;div&gt;
      &lt;strong&gt;What you will build&lt;/strong&gt;
      &lt;p&gt;A 3D cylindrical carousel where 8 panels are placed around a circle using trigonometry. Drag velocity is applied as angular momentum with a 0.92 friction factor, so the carousel decelerates naturally and the closest panel snaps to front using a short easing interpolation.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;section class=&quot;jh-exp-stage&quot; id=&quot;demo-threejs_3d_carousel_drag&quot;&gt;
  &lt;div class=&quot;jh-exp-stage-head&quot;&gt;
    &lt;div&gt;&lt;span class=&quot;jh-exp-kicker&quot;&gt;LIVE 3D EXPERIMENT&lt;/span&gt;&lt;h2&gt;Three.js 3D Carousel with Drag Interaction&lt;/h2&gt;&lt;/div&gt;
    &lt;span class=&quot;jh-exp-hint&quot;&gt;Drag or move the pointer inside the 3D view&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-exp-canvas-wrap&quot; data-three-host=&quot;1&quot;&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;script type=&quot;module&quot;&gt;
(async()=&gt;{
  const root=document.getElementById(&#39;demo-threejs_3d_carousel_drag&#39;);
  const host=root.querySelector(&#39;[data-three-host]&#39;);
  try{
    const THREE=await import(&#39;https://cdn.jsdelivr.net/npm/three@0.185.1/build/three.module.js&#39;);
    const scene=new THREE.Scene();
    scene.background=new THREE.Color(0x020a10);
    const camera=new THREE.PerspectiveCamera(48,1,.1,100);
    camera.position.set(0,0,6);
    const renderer=new THREE.WebGLRenderer({antialias:true});
    renderer.setPixelRatio(Math.min(devicePixelRatio,2));
    host.appendChild(renderer.domElement);
    function resize(){
      const r=host.getBoundingClientRect();renderer.setSize(r.width,r.height,false);camera.aspect=r.width/r.height;camera.updateProjectionMatrix();
    }
    resize();new ResizeObserver(resize).observe(host);
    
const group=new THREE.Group();scene.add(group);
for(let i=0;i&lt;7;i++){const geo=new THREE.BoxGeometry(1.05,1.45,.08);const mat=new THREE.MeshStandardMaterial({color:new THREE.Color().setHSL(.56+i*.035,.72,.45)});const card=new THREE.Mesh(geo,mat);const a=i/7*Math.PI*2;card.position.set(Math.sin(a)*2.6,0,Math.cos(a)*2.6);card.lookAt(0,0,0);group.add(card);}
scene.add(new THREE.HemisphereLight(0xffffff,0x102030,3));
let drag=0,last=0;
host.addEventListener(&#39;pointerdown&#39;,e=&gt;{last=e.clientX;host.setPointerCapture(e.pointerId)});
host.addEventListener(&#39;pointermove&#39;,e=&gt;{if(e.buttons){drag+=(e.clientX-last)*.006;last=e.clientX;}});
function tick(t){group.rotation.y+=(drag-group.rotation.y)*.06;}

    function loop(t){tick(t);renderer.render(scene,camera);requestAnimationFrame(loop)}requestAnimationFrame(loop);
  }catch(err){
    host.innerHTML=&#39;&lt;div style=&quot;padding:40px;color:#9db1bb;text-align:center&quot;&gt;3D demo unavailable in this browser.&lt;/div&gt;&#39;;
  }
})();
&lt;/script&gt;

  &lt;h2&gt;How the Three.js 3D carousel drag works&lt;/h2&gt;
  &lt;p&gt;Position N panels at &lt;code&gt;angle = (i / N) * Math.PI * 2&lt;/code&gt; around a radius using &lt;code&gt;Math.sin(angle) * radius&lt;/code&gt; for X and &lt;code&gt;Math.cos(angle) * radius&lt;/code&gt; for Z. On &lt;code&gt;pointermove&lt;/code&gt;, measure the horizontal delta between the current and previous pointer positions and add it to a &lt;code&gt;rotationSpeed&lt;/code&gt; variable. Each frame, add &lt;code&gt;rotationSpeed&lt;/code&gt; to the group&#39;s &lt;code&gt;rotation.y&lt;/code&gt; and multiply &lt;code&gt;rotationSpeed&lt;/code&gt; by the friction factor (0.92).&lt;/p&gt;
  &lt;p&gt;The panel closest to the camera (the one with &lt;code&gt;rotation.y ≈ 0&lt;/code&gt;) gets a scale of 1.15 to visually pop it forward. Compute which panel is &quot;in front&quot; by finding the one whose world angle is closest to 0 after normalizing for the group rotation. After drag ends, lerp the rotation toward the nearest snap angle to settle it cleanly.&lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;
    &lt;div class=&quot;jh-exp-code-head&quot;&gt;&lt;span&gt;scene.js&lt;/span&gt;&lt;span&gt;KEY IDEA&lt;/span&gt;&lt;/div&gt;
    &lt;pre&gt;&lt;code&gt;const angle = index / cardCount * Math.PI * 2;

card.position.set(
  Math.sin(angle) * radius,
  0,
  Math.cos(angle) * radius
);

card.lookAt(0, 0, 0);&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
  &lt;p class=&quot;jh-exp-caption&quot;&gt;The cylinder layout is six trig lines. Drag physics is two lines: add delta to speed, multiply speed by friction each frame. The rest is Three.js scene setup.&lt;/p&gt;

  &lt;h2&gt;Why Three.js for a carousel instead of CSS-only&lt;/h2&gt;
  &lt;p&gt;CSS-only carousels with &lt;code&gt;scroll-snap&lt;/code&gt; are excellent for flat layouts. Three.js is justified when you need perspective depth, per-panel 3D transforms, custom lighting on panels (for product showcase), or drag-with-momentum that feels physically natural rather than scroll-snappy. The 3D cylindrical layout is impossible in CSS without a fixed panel count and manually computed &lt;code&gt;rotateY&lt;/code&gt; values per item.&lt;/p&gt;
  &lt;p&gt;This carousel also handles variable panel content naturally: each &lt;code&gt;Mesh&lt;/code&gt; can have its own texture, color, or geometry. Add &lt;code&gt;THREE.MeshStandardMaterial&lt;/code&gt; with a &lt;code&gt;map&lt;/code&gt; texture and the panels become a product gallery with real lighting. The drag and momentum code stays identical regardless of what&#39;s on the panels.&lt;/p&gt;

  &lt;div class=&quot;jh-article-current&quot;&gt;
    &lt;strong&gt;Performance note&lt;/strong&gt;
    &lt;p&gt;Keep panel geometry low-poly—each panel is a &lt;code&gt;PlaneGeometry(2, 2.5, 1, 1)&lt;/code&gt; (4 vertices). Texture maps should be power-of-two dimensions (512×512, 1024×512) for optimal GPU memory alignment. Dispose of unused textures with &lt;code&gt;texture.dispose()&lt;/code&gt; when panels scroll out of view permanently.&lt;/p&gt;
  &lt;/div&gt;

  &lt;h2&gt;Extensions and variations&lt;/h2&gt;
  &lt;p&gt;Add a &lt;code&gt;raycaster&lt;/code&gt; click handler to detect which panel the user taps and trigger a view transition to a detail page. Replace flat planes with angled panels for a fan-carousel style. Add a subtle vertical parallax by moving the camera Y position slightly based on the device gyroscope for an AR-like depth effect on mobile.&lt;/p&gt;

  &lt;h2&gt;Frequently asked questions&lt;/h2&gt;
  &lt;h3&gt;Can I use this 3D carousel with real images or video?&lt;/h3&gt;
  &lt;p&gt;Yes. Pass a &lt;code&gt;THREE.TextureLoader&lt;/code&gt; texture to &lt;code&gt;MeshStandardMaterial.map&lt;/code&gt; for images, or a &lt;code&gt;THREE.VideoTexture&lt;/code&gt; for video panels. The carousel code is geometry-agnostic—the positioning and drag logic work regardless of what the material shows.&lt;/p&gt;
  &lt;h3&gt;How do I snap to the nearest panel after release?&lt;/h3&gt;
  &lt;p&gt;After the drag ends (on &lt;code&gt;pointerup&lt;/code&gt;), find the panel index whose world angle is closest to 0. Compute the target rotation as &lt;code&gt;-(index / N) * Math.PI * 2&lt;/code&gt;. Lerp the group rotation toward that target each frame at a factor of 0.08–0.12 until the difference is below 0.001.&lt;/p&gt;

  &lt;h2&gt;Related experiments&lt;/h2&gt;
  &lt;ul&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-particle-cursor-effect-canvas.html&quot;&gt;JavaScript Particle Cursor Effect with Canvas&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-magnetic-button-effect.html&quot;&gt;JavaScript Magnetic Button Effect&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/threejs-particle-cursor-effect.html&quot;&gt;Three.js Particle Cursor Effect&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;

  &lt;div class=&quot;jh-article-end&quot;&gt;
    &lt;p&gt;Part of a weekly series on visual JavaScript, modern CSS, browser graphics, and HTML5 games&amp;mdash;one live experiment per post, production-ready code snippets, and the reasoning behind each technique.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;</description><link>http://juegosenhtml5.blogspot.com/2026/09/threejs-3d-carousel-drag.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-9165659541366476635</guid><pubDate>Thu, 03 Sep 2026 15:30:00 +0000</pubDate><atom:updated>2026-09-03T10:30:00.118-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">canvas</category><category domain="http://www.blogger.com/atom/ns#">en-games</category><category domain="http://www.blogger.com/atom/ns#">game VFX</category><category domain="http://www.blogger.com/atom/ns#">Games</category><category domain="http://www.blogger.com/atom/ns#">HTML5 Game</category><category domain="http://www.blogger.com/atom/ns#">JavaScript</category><category domain="http://www.blogger.com/atom/ns#">lang-en</category><title>JavaScript Neon Trail Effect for HTML5 Games — Hero Ability &amp; Cursor VFX</title><description>&lt;!--
BLOGGER POST — EN
Title / H1: JavaScript Neon Trail Effect for HTML5 Games — Hero Ability &amp; Cursor VFX
Suggested slug: javascript-neon-mouse-trail
Search description: Build a glowing neon trail effect in JavaScript Canvas for HTML5 games — hero ability trails, sci-fi cursor FX, and lightsaber-style effects. Under 1KB.
Primary keyword: JavaScript neon trail effect HTML5 game
Secondary keywords: HTML5 game trail effect, JavaScript glow trail, game cursor VFX, neon trail JavaScript, Canvas trail animation game
Labels: lang-en, en-games, Games, JavaScript, Canvas, game VFX, HTML5 Game
Series: HTML5 Games from Scratch
Social title: Neon Game Trail Effect in JavaScript — Hero Ability VFX
Thumbnail text: TRAIL!
--&gt;
&lt;article class=&quot;jh-exp&quot; data-project=&quot;javascript-neon-mouse-trail&quot; data-primary-keyword=&quot;JavaScript neon trail effect HTML5 game&quot;&gt;
  &lt;p class=&quot;jh-article-lead&quot;&gt;&lt;strong&gt;Every hero ability needs a trail.&lt;/strong&gt; This neon trail effect — each glowing dot fades from electric cyan to black with a motion-blur afterglow — is the exact visual pattern used for lightsabers, energy weapons, dash abilities, and sci-fi cursors in HTML5 games. Under 1 KB of vanilla JavaScript, zero libraries, perfect on touch via &lt;code&gt;pointermove&lt;/code&gt;.&lt;/p&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;
    &lt;div&gt;
      &lt;strong&gt;What you will build&lt;/strong&gt;
      &lt;p&gt;A glowing trail of 20 fading dots that follows the cursor with a staggered delay. Each dot is 10% smaller than the previous, colored with decreasing opacity. A semi-transparent background fill on each frame creates the motion-blur afterglow effect.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;section class=&quot;jh-exp-stage&quot; id=&quot;demo-javascript_neon_mouse_trail&quot;&gt;
  &lt;div class=&quot;jh-exp-stage-head&quot;&gt;
    &lt;div&gt;&lt;span class=&quot;jh-exp-kicker&quot;&gt;LIVE EXPERIMENT&lt;/span&gt;&lt;h2&gt;Neon mouse trail&lt;/h2&gt;&lt;/div&gt;
    &lt;span class=&quot;jh-exp-hint&quot;&gt;Move the pointer to draw light&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-exp-canvas-wrap&quot;&gt;
    &lt;canvas class=&quot;jh-exp-canvas&quot; aria-label=&quot;Neon mouse trail interactive demo&quot;&gt;&lt;/canvas&gt;
  &lt;/div&gt;
  
&lt;/section&gt;
&lt;script&gt;
(function(){
  const root=document.getElementById(&#39;demo-javascript_neon_mouse_trail&#39;);
  const canvas=root.querySelector(&#39;canvas&#39;);
  const ctx=canvas.getContext(&#39;2d&#39;);
  function resize(){
    const r=canvas.getBoundingClientRect();
    const dpr=Math.min(window.devicePixelRatio||1,2);
    canvas.width=Math.max(2,Math.floor(r.width*dpr));
    canvas.height=Math.max(2,Math.floor(r.height*dpr));
    ctx.setTransform(dpr,0,0,dpr,0,0);
  }
  resize();
  new ResizeObserver(resize).observe(canvas);
  
let pts=[];canvas.addEventListener(&#39;pointermove&#39;,e=&gt;{const r=canvas.getBoundingClientRect();pts.push({x:e.clientX-r.left,y:e.clientY-r.top,a:1});if(pts.length&gt;90)pts.shift()});
function frame(){const w=canvas.clientWidth,h=canvas.clientHeight;ctx.fillStyle=&#39;rgba(2,10,16,.18)&#39;;ctx.fillRect(0,0,w,h);ctx.globalCompositeOperation=&#39;lighter&#39;;for(let i=1;i&lt;pts.length;i++){const p=pts[i],q=pts[i-1];p.a*=.965;ctx.strokeStyle=`rgba(${30+i*2},${160+i},255,${p.a*.7})`;ctx.lineWidth=1+i/20;ctx.beginPath();ctx.moveTo(q.x,q.y);ctx.lineTo(p.x,p.y);ctx.stroke()}ctx.globalCompositeOperation=&#39;source-over&#39;;requestAnimationFrame(frame)}frame();

})();
&lt;/script&gt;

  &lt;h2&gt;How the JavaScript neon mouse trail works&lt;/h2&gt;
  &lt;p&gt;Store the last 20 cursor positions in a ring buffer (a fixed-size array where new values overwrite the oldest). Each &lt;code&gt;requestAnimationFrame&lt;/code&gt; tick: draw a rectangle over the whole canvas with alpha ~0.18—this creates the fade-out effect by partially erasing the previous frame rather than clearing it. Then iterate the buffer drawing each point as an arc with radius and opacity proportional to its index. Older points are smaller and more transparent.&lt;/p&gt;
  &lt;p&gt;The key technique is &lt;code&gt;ctx.globalCompositeOperation = &#39;lighter&#39;&lt;/code&gt; combined with low-alpha color values. This additive blending makes overlapping dots bloom brighter instead of stacking to a solid color, which creates the neon glow without any blur filter (blur filters are expensive on Canvas). The trail appears to emit light at the cursor tip and decay to darkness behind it.&lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;
    &lt;div class=&quot;jh-exp-code-head&quot;&gt;&lt;span&gt;demo.js&lt;/span&gt;&lt;span&gt;KEY IDEA&lt;/span&gt;&lt;/div&gt;
    &lt;pre&gt;&lt;code&gt;points.push({ x, y, alpha: 1 });

ctx.globalCompositeOperation = &amp;#x27;lighter&amp;#x27;;
points.forEach((point, index) =&amp;gt; {
  point.alpha *= 0.965;
  ctx.strokeStyle = `rgba(30, 200, 255, ${point.alpha})`;
});&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
  &lt;p class=&quot;jh-exp-caption&quot;&gt;Two details carry the whole aesthetic: &lt;code&gt;globalCompositeOperation = &#39;lighter&#39;&lt;/code&gt; for additive glow, and &lt;code&gt;alpha *= 0.965&lt;/code&gt; per frame for the fade. Change the alpha multiplier to tune the trail length.&lt;/p&gt;

  &lt;h2&gt;Why mouse trails still work in 2026&lt;/h2&gt;
  &lt;p&gt;Mouse trails never went out of style—they just evolved. The 2000s version was a cheesy GIF overlay. The 2026 version is a compositing-mode Canvas animation under 1 KB that responds to both mouse and touch, respects &lt;code&gt;prefers-reduced-motion&lt;/code&gt;, and can be layered over any content with &lt;code&gt;pointer-events: none&lt;/code&gt;. Studios use them on agency sites and product launches to signal that every pixel is intentional.&lt;/p&gt;
  &lt;p&gt;This demo also teaches three transferable Canvas patterns: ring buffers for time-series data, partial-erase animation (vs. full clear) for motion blur, and additive blending for glow without blur filters. All three appear in game engines, data visualization libraries, and generative art tools.&lt;/p&gt;

  &lt;div class=&quot;jh-article-current&quot;&gt;
    &lt;strong&gt;Performance note&lt;/strong&gt;
    &lt;p&gt;20 arc draws per frame is trivially cheap. If you extend to 100+ trail points for a longer tail, batch the paths: call &lt;code&gt;ctx.beginPath()&lt;/code&gt; once, chain all &lt;code&gt;arc()&lt;/code&gt; calls, then &lt;code&gt;ctx.fill()&lt;/code&gt; once. This reduces Canvas state-change overhead from O(n) to O(1).&lt;/p&gt;
  &lt;/div&gt;

  &lt;h2&gt;Ways to extend this effect&lt;/h2&gt;
  &lt;p&gt;Swap the cyan color for a rainbow gradient by cycling through HSL hues based on trail index. Add a splatter effect on click: burst 10 short-lived particles outward from the click point using the same ring-buffer pattern. Combine with the Three.js Tubes Cursor Trail post to see how a 3D version of the same idea looks with geometry.&lt;/p&gt;

  &lt;h2&gt;Frequently asked questions&lt;/h2&gt;
  &lt;h3&gt;How do I make the trail follow touch on mobile?&lt;/h3&gt;
  &lt;p&gt;The &lt;code&gt;pointermove&lt;/code&gt; event fires for both mouse and touch on modern browsers. No separate touch handler is needed—just listen to &lt;code&gt;canvas.addEventListener(&#39;pointermove&#39;, handler)&lt;/code&gt;. Set &lt;code&gt;touch-action: none&lt;/code&gt; on the canvas to prevent the browser from scrolling while the trail is active.&lt;/p&gt;
  &lt;h3&gt;How do I change the trail color?&lt;/h3&gt;
  &lt;p&gt;Replace the &lt;code&gt;rgba(30, 200, 255, ...)&lt;/code&gt; value with any color. Use HSL for easy hue cycling: &lt;code&gt;hsl(${180 + index * 3}, 100%, 60%)&lt;/code&gt; gives a rainbow trail that shifts color along its length. Keep alpha below 0.8 so the additive blending creates bloom rather than a solid stripe.&lt;/p&gt;

  &lt;h2&gt;Related experiments&lt;/h2&gt;
  &lt;ul&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-particle-cursor-effect-canvas.html&quot;&gt;JavaScript Particle Cursor Effect with Canvas&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-magnetic-button-effect.html&quot;&gt;JavaScript Magnetic Button Effect&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/threejs-particle-cursor-effect.html&quot;&gt;Three.js Particle Cursor Effect&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;

  &lt;div class=&quot;jh-article-end&quot;&gt;
    &lt;p&gt;Part of a weekly series on visual JavaScript, modern CSS, browser graphics, and HTML5 games&amp;mdash;one live experiment per post, production-ready code snippets, and the reasoning behind each technique.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;</description><link>http://juegosenhtml5.blogspot.com/2026/09/javascript-neon-mouse-trail.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-7650809051860223353</guid><pubDate>Thu, 03 Sep 2026 12:11:00 +0000</pubDate><atom:updated>2026-09-03T07:11:00.118-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">en-games</category><category domain="http://www.blogger.com/atom/ns#">game VFX</category><category domain="http://www.blogger.com/atom/ns#">Games</category><category domain="http://www.blogger.com/atom/ns#">HTML5 Game</category><category domain="http://www.blogger.com/atom/ns#">lang-en</category><category domain="http://www.blogger.com/atom/ns#">particles</category><category domain="http://www.blogger.com/atom/ns#">Three.js</category><category domain="http://www.blogger.com/atom/ns#">WebGL</category><title>Three.js 3D Particle Effect for Games — WebGL Spell &amp; Explosion VFX</title><description>&lt;!--
BLOGGER POST — EN
Title / H1: Three.js 3D Particle Effect for Games — WebGL Spell &amp; Explosion VFX
Suggested slug: threejs-particle-cursor-effect
Search description: Build a Three.js 3D particle VFX system for HTML5 games — 4000 WebGL particles, explosion and magic spell effects, game-ready performance. No assets needed.
Primary keyword: Three.js particle effect HTML5 game WebGL
Secondary keywords: Three.js game VFX, WebGL particle explosion, 3D game effect JavaScript, Three.js spell effect, HTML5 game particle system
Labels: lang-en, en-games, Games, Three.js, WebGL, particles, game VFX, HTML5 Game
Series: HTML5 Games from Scratch
Social title: 3D Game Particle VFX in Three.js — 4000 WebGL Particles
Thumbnail text: 3D VFX!
--&gt;
&lt;article class=&quot;jh-exp&quot; data-project=&quot;threejs-particle-cursor-effect&quot; data-primary-keyword=&quot;Three.js particle effect HTML5 game WebGL&quot;&gt;
  &lt;p class=&quot;jh-article-lead&quot;&gt;&lt;strong&gt;This is what 3D game particle VFX looks like in the browser.&lt;/strong&gt; 4 000 particles orbit through WebGL space — move your cursor and a displacement wave ripples outward exactly like a magic spell or shockwave hit in a 3D HTML5 game. Three.js handles the GPU rendering; you write the physics. The same technique powers particle explosions, portal effects, and enemy death bursts in browser-based 3D games.&lt;/p&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;
    &lt;div&gt;
      &lt;strong&gt;What you will build&lt;/strong&gt;
      &lt;p&gt;A &lt;code&gt;BufferGeometry&lt;/code&gt;-powered particle cloud where the cursor position is projected onto a 3D plane and a radial force field repels nearby particles. Each particle stores position and velocity in a &lt;code&gt;Float32Array&lt;/code&gt; updated entirely on the CPU each frame.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;section class=&quot;jh-exp-stage&quot; id=&quot;demo-threejs_particle_cursor_effect&quot;&gt;
  &lt;div class=&quot;jh-exp-stage-head&quot;&gt;
    &lt;div&gt;&lt;span class=&quot;jh-exp-kicker&quot;&gt;LIVE 3D EXPERIMENT&lt;/span&gt;&lt;h2&gt;Three.js Particle Cursor Effect&lt;/h2&gt;&lt;/div&gt;
    &lt;span class=&quot;jh-exp-hint&quot;&gt;Drag or move the pointer inside the 3D view&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-exp-canvas-wrap&quot; data-three-host=&quot;1&quot;&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;script type=&quot;module&quot;&gt;
(async()=&gt;{
  const root=document.getElementById(&#39;demo-threejs_particle_cursor_effect&#39;);
  const host=root.querySelector(&#39;[data-three-host]&#39;);
  try{
    const THREE=await import(&#39;https://cdn.jsdelivr.net/npm/three@0.185.1/build/three.module.js&#39;);
    const scene=new THREE.Scene();
    scene.background=new THREE.Color(0x020a10);
    const camera=new THREE.PerspectiveCamera(48,1,.1,100);
    camera.position.set(0,0,6);
    const renderer=new THREE.WebGLRenderer({antialias:true});
    renderer.setPixelRatio(Math.min(devicePixelRatio,2));
    host.appendChild(renderer.domElement);
    function resize(){
      const r=host.getBoundingClientRect();renderer.setSize(r.width,r.height,false);camera.aspect=r.width/r.height;camera.updateProjectionMatrix();
    }
    resize();new ResizeObserver(resize).observe(host);
    
const count=3600;
const g=new THREE.BufferGeometry();
const pos=new Float32Array(count*3);
for(let i=0;i&lt;count;i++){const a=Math.random()*Math.PI*2,r=Math.sqrt(Math.random())*2.6;pos[i*3]=Math.cos(a)*r;pos[i*3+1]=Math.sin(a)*r;pos[i*3+2]=(Math.random()-.5)*.6;}
g.setAttribute(&#39;position&#39;,new THREE.BufferAttribute(pos,3));
const mat=new THREE.PointsMaterial({size:.028,color:0x35d8ff,transparent:true,opacity:.9});
const pts=new THREE.Points(g,mat);scene.add(pts);
let mx=0,my=0;
host.addEventListener(&#39;pointermove&#39;,e=&gt;{const r=host.getBoundingClientRect();mx=(e.clientX-r.left)/r.width-.5;my=(e.clientY-r.top)/r.height-.5;});
function tick(t){pts.rotation.z=t*.00008;pts.rotation.x+=(my*.7-pts.rotation.x)*.025;pts.rotation.y+=(mx*.7-pts.rotation.y)*.025;}

    function loop(t){tick(t);renderer.render(scene,camera);requestAnimationFrame(loop)}requestAnimationFrame(loop);
  }catch(err){
    host.innerHTML=&#39;&lt;div style=&quot;padding:40px;color:#9db1bb;text-align:center&quot;&gt;3D demo unavailable in this browser.&lt;/div&gt;&#39;;
  }
})();
&lt;/script&gt;

  &lt;h2&gt;How the Three.js particle cursor effect works&lt;/h2&gt;
  &lt;p&gt;Create a &lt;code&gt;THREE.BufferGeometry&lt;/code&gt; and attach a &lt;code&gt;Float32Array&lt;/code&gt; of 4 000 × 3 floats as the &lt;code&gt;position&lt;/code&gt; attribute. A parallel array stores velocities. Each frame, use a &lt;code&gt;THREE.Raycaster&lt;/code&gt; to project the normalized mouse coordinates (NDC) onto a plane at z = 0 to get the 3D intersection point. For each particle within the repulsion radius, compute the distance, scale the impulse inversely, and add it to the velocity. Damp all velocities by 0.92. Update the position attribute and set &lt;code&gt;needsUpdate = true&lt;/code&gt;.&lt;/p&gt;
  &lt;p&gt;The jump from Canvas 2D to Three.js WebGL is not just about particle count. It unlocks additive blending (&lt;code&gt;blending: THREE.AdditiveBlending&lt;/code&gt;) which makes dense clusters bloom white—an effect that is physically expensive to fake in 2D Canvas. It also opens the door to vertex shaders that color each particle by speed or distance from center without any per-particle JS color logic.&lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;
    &lt;div class=&quot;jh-exp-code-head&quot;&gt;&lt;span&gt;scene.js&lt;/span&gt;&lt;span&gt;KEY IDEA&lt;/span&gt;&lt;/div&gt;
    &lt;pre&gt;&lt;code&gt;const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
  &amp;#x27;position&amp;#x27;,
  new THREE.BufferAttribute(positions, 3)
);

const points = new THREE.Points(
  geometry,
  new THREE.PointsMaterial({ size: 0.028 })
);&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
  &lt;p class=&quot;jh-exp-caption&quot;&gt;All 4 000 particle positions live in one &lt;code&gt;Float32Array&lt;/code&gt; sent to the GPU as a single buffer. The key line after updating it: &lt;code&gt;geometry.attributes.position.needsUpdate = true&lt;/code&gt;.&lt;/p&gt;

  &lt;h2&gt;Canvas 2D vs Three.js particles: when to use which&lt;/h2&gt;
  &lt;p&gt;Canvas 2D handles 200–800 particles comfortably on any device. Three.js WebGL handles 10 000–100 000, with the upper limit set by your vertex shader complexity and GPU fill rate rather than CPU cycles. Use Canvas 2D when you need simplicity and maximum browser compatibility. Use Three.js when you need scale, depth, custom shaders, or scene integration with 3D objects.&lt;/p&gt;
  &lt;p&gt;For this demo, 4 000 particles is the sweet spot: visually dense enough to feel like a cloud, simple enough that a mid-range phone can sustain 60 fps. The repulsion radius and force strength are the two tuning knobs that control how the cloud responds—wider radius for a softer &quot;breath,&quot; higher force for a sharp scatter.&lt;/p&gt;

  &lt;div class=&quot;jh-article-current&quot;&gt;
    &lt;strong&gt;Performance note&lt;/strong&gt;
    &lt;p&gt;Never construct a new &lt;code&gt;Float32Array&lt;/code&gt; inside the animation loop—allocate once at init time and overwrite values in-place. Call &lt;code&gt;geometry.attributes.position.needsUpdate = true&lt;/code&gt; after each update, not before. Cap &lt;code&gt;renderer.setPixelRatio&lt;/code&gt; to 2 on Retina screens; 3× or 4× DPR triples the fragment shader work for minimal visual gain.&lt;/p&gt;
  &lt;/div&gt;

  &lt;h2&gt;Where to take this next&lt;/h2&gt;
  &lt;p&gt;Replace the &lt;code&gt;PointsMaterial&lt;/code&gt; with a custom &lt;code&gt;ShaderMaterial&lt;/code&gt; that reads each particle&#39;s velocity magnitude as a uniform and maps it to color—fast-moving particles glow white, slow ones fade to blue. Add a secondary attractor point that orbits the center to give the cloud a vortex shape at rest. Or scale to 20 000 particles and move the physics to a compute shader using WebGPU.&lt;/p&gt;

  &lt;h2&gt;Frequently asked questions&lt;/h2&gt;
  &lt;h3&gt;How is this different from the Canvas 2D particle cursor post?&lt;/h3&gt;
  &lt;p&gt;The Canvas version uses 520 particles on the CPU with 2D drawing. This Three.js version uses 4 000 particles on the CPU with WebGL rendering, additive blending, and perspective projection. The physics formula is nearly identical—comparing both posts side by side is an excellent way to understand when WebGL overhead is justified by the visual payoff.&lt;/p&gt;
  &lt;h3&gt;Can I use a texture for each particle instead of a dot?&lt;/h3&gt;
  &lt;p&gt;Yes. Pass a &lt;code&gt;THREE.TextureLoader&lt;/code&gt;-loaded sprite to &lt;code&gt;PointsMaterial.map&lt;/code&gt; and set &lt;code&gt;transparent: true, alphaTest: 0.01&lt;/code&gt;. Use a white radial gradient PNG for soft circular glows, or a star sprite for a starfield effect. The rest of the physics code stays identical.&lt;/p&gt;

  &lt;h2&gt;Related experiments&lt;/h2&gt;
  &lt;ul&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-particle-cursor-effect-canvas.html&quot;&gt;JavaScript Particle Cursor Effect with Canvas&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-magnetic-button-effect.html&quot;&gt;JavaScript Magnetic Button Effect&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-neon-mouse-trail.html&quot;&gt;JavaScript Neon Mouse Trail Animation&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;

  &lt;div class=&quot;jh-article-end&quot;&gt;
    &lt;p&gt;Part of a weekly series on visual JavaScript, modern CSS, browser graphics, and HTML5 games—one live experiment per post, production-ready code snippets, and the reasoning behind each technique.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;</description><link>http://juegosenhtml5.blogspot.com/2026/09/threejs-particle-cursor-effect_01423283305.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-2442245113373974005</guid><pubDate>Wed, 02 Sep 2026 22:39:23 +0000</pubDate><atom:updated>2026-09-02T17:39:23.838-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">JavaScript neon trail effect HTML5 game</category><title>JavaScript Neon Trail Effect for HTML5 Games — Hero Ability &amp; Cursor VFX</title><description>&lt;!--
BLOGGER POST — EN
Title / H1: JavaScript Neon Trail Effect for HTML5 Games — Hero Ability &amp; Cursor VFX
Suggested slug: javascript-neon-mouse-trail
Search description: Build a glowing neon trail effect in JavaScript Canvas for HTML5 games — hero ability trails, sci-fi cursor FX, and lightsaber-style effects. Under 1KB.
Primary keyword: JavaScript neon trail effect HTML5 game
Secondary keywords: HTML5 game trail effect, JavaScript glow trail, game cursor VFX, neon trail JavaScript, Canvas trail animation game
Labels: lang-en, en-games, Games, JavaScript, Canvas, game VFX, HTML5 Game
Series: HTML5 Games from Scratch
Social title: Neon Game Trail Effect in JavaScript — Hero Ability VFX
Thumbnail text: TRAIL!
--&gt;
&lt;article class=&quot;jh-exp&quot; data-project=&quot;javascript-neon-mouse-trail&quot; data-primary-keyword=&quot;JavaScript neon trail effect HTML5 game&quot;&gt;
  &lt;p class=&quot;jh-article-lead&quot;&gt;&lt;strong&gt;Every hero ability needs a trail.&lt;/strong&gt; This neon trail effect — each glowing dot fades from electric cyan to black with a motion-blur afterglow — is the exact visual pattern used for lightsabers, energy weapons, dash abilities, and sci-fi cursors in HTML5 games. Under 1 KB of vanilla JavaScript, zero libraries, perfect on touch via &lt;code&gt;pointermove&lt;/code&gt;.&lt;/p&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;
    &lt;div&gt;
      &lt;strong&gt;What you will build&lt;/strong&gt;
      &lt;p&gt;A glowing trail of 20 fading dots that follows the cursor with a staggered delay. Each dot is 10% smaller than the previous, colored with decreasing opacity. A semi-transparent background fill on each frame creates the motion-blur afterglow effect.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;section class=&quot;jh-exp-stage&quot; id=&quot;demo-javascript_neon_mouse_trail&quot;&gt;
  &lt;div class=&quot;jh-exp-stage-head&quot;&gt;
    &lt;div&gt;&lt;span class=&quot;jh-exp-kicker&quot;&gt;LIVE EXPERIMENT&lt;/span&gt;&lt;h2&gt;Neon mouse trail&lt;/h2&gt;&lt;/div&gt;
    &lt;span class=&quot;jh-exp-hint&quot;&gt;Move the pointer to draw light&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-exp-canvas-wrap&quot;&gt;
    &lt;canvas class=&quot;jh-exp-canvas&quot; aria-label=&quot;Neon mouse trail interactive demo&quot;&gt;&lt;/canvas&gt;
  &lt;/div&gt;
  
&lt;/section&gt;
&lt;script&gt;
(function(){
  const root=document.getElementById(&#39;demo-javascript_neon_mouse_trail&#39;);
  const canvas=root.querySelector(&#39;canvas&#39;);
  const ctx=canvas.getContext(&#39;2d&#39;);
  function resize(){
    const r=canvas.getBoundingClientRect();
    const dpr=Math.min(window.devicePixelRatio||1,2);
    canvas.width=Math.max(2,Math.floor(r.width*dpr));
    canvas.height=Math.max(2,Math.floor(r.height*dpr));
    ctx.setTransform(dpr,0,0,dpr,0,0);
  }
  resize();
  new ResizeObserver(resize).observe(canvas);
  
let pts=[];canvas.addEventListener(&#39;pointermove&#39;,e=&gt;{const r=canvas.getBoundingClientRect();pts.push({x:e.clientX-r.left,y:e.clientY-r.top,a:1});if(pts.length&gt;90)pts.shift()});
function frame(){const w=canvas.clientWidth,h=canvas.clientHeight;ctx.fillStyle=&#39;rgba(2,10,16,.18)&#39;;ctx.fillRect(0,0,w,h);ctx.globalCompositeOperation=&#39;lighter&#39;;for(let i=1;i&lt;pts.length;i++){const p=pts[i],q=pts[i-1];p.a*=.965;ctx.strokeStyle=`rgba(${30+i*2},${160+i},255,${p.a*.7})`;ctx.lineWidth=1+i/20;ctx.beginPath();ctx.moveTo(q.x,q.y);ctx.lineTo(p.x,p.y);ctx.stroke()}ctx.globalCompositeOperation=&#39;source-over&#39;;requestAnimationFrame(frame)}frame();

})();
&lt;/script&gt;

  &lt;h2&gt;How the JavaScript neon mouse trail works&lt;/h2&gt;
  &lt;p&gt;Store the last 20 cursor positions in a ring buffer (a fixed-size array where new values overwrite the oldest). Each &lt;code&gt;requestAnimationFrame&lt;/code&gt; tick: draw a rectangle over the whole canvas with alpha ~0.18—this creates the fade-out effect by partially erasing the previous frame rather than clearing it. Then iterate the buffer drawing each point as an arc with radius and opacity proportional to its index. Older points are smaller and more transparent.&lt;/p&gt;
  &lt;p&gt;The key technique is &lt;code&gt;ctx.globalCompositeOperation = &#39;lighter&#39;&lt;/code&gt; combined with low-alpha color values. This additive blending makes overlapping dots bloom brighter instead of stacking to a solid color, which creates the neon glow without any blur filter (blur filters are expensive on Canvas). The trail appears to emit light at the cursor tip and decay to darkness behind it.&lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;
    &lt;div class=&quot;jh-exp-code-head&quot;&gt;&lt;span&gt;demo.js&lt;/span&gt;&lt;span&gt;KEY IDEA&lt;/span&gt;&lt;/div&gt;
    &lt;pre&gt;&lt;code&gt;points.push({ x, y, alpha: 1 });

ctx.globalCompositeOperation = &amp;#x27;lighter&amp;#x27;;
points.forEach((point, index) =&amp;gt; {
  point.alpha *= 0.965;
  ctx.strokeStyle = `rgba(30, 200, 255, ${point.alpha})`;
});&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
  &lt;p class=&quot;jh-exp-caption&quot;&gt;Two details carry the whole aesthetic: &lt;code&gt;globalCompositeOperation = &#39;lighter&#39;&lt;/code&gt; for additive glow, and &lt;code&gt;alpha *= 0.965&lt;/code&gt; per frame for the fade. Change the alpha multiplier to tune the trail length.&lt;/p&gt;

  &lt;h2&gt;Why mouse trails still work in 2026&lt;/h2&gt;
  &lt;p&gt;Mouse trails never went out of style—they just evolved. The 2000s version was a cheesy GIF overlay. The 2026 version is a compositing-mode Canvas animation under 1 KB that responds to both mouse and touch, respects &lt;code&gt;prefers-reduced-motion&lt;/code&gt;, and can be layered over any content with &lt;code&gt;pointer-events: none&lt;/code&gt;. Studios use them on agency sites and product launches to signal that every pixel is intentional.&lt;/p&gt;
  &lt;p&gt;This demo also teaches three transferable Canvas patterns: ring buffers for time-series data, partial-erase animation (vs. full clear) for motion blur, and additive blending for glow without blur filters. All three appear in game engines, data visualization libraries, and generative art tools.&lt;/p&gt;

  &lt;div class=&quot;jh-article-current&quot;&gt;
    &lt;strong&gt;Performance note&lt;/strong&gt;
    &lt;p&gt;20 arc draws per frame is trivially cheap. If you extend to 100+ trail points for a longer tail, batch the paths: call &lt;code&gt;ctx.beginPath()&lt;/code&gt; once, chain all &lt;code&gt;arc()&lt;/code&gt; calls, then &lt;code&gt;ctx.fill()&lt;/code&gt; once. This reduces Canvas state-change overhead from O(n) to O(1).&lt;/p&gt;
  &lt;/div&gt;

  &lt;h2&gt;Ways to extend this effect&lt;/h2&gt;
  &lt;p&gt;Swap the cyan color for a rainbow gradient by cycling through HSL hues based on trail index. Add a splatter effect on click: burst 10 short-lived particles outward from the click point using the same ring-buffer pattern. Combine with the Three.js Tubes Cursor Trail post to see how a 3D version of the same idea looks with geometry.&lt;/p&gt;

  &lt;h2&gt;Frequently asked questions&lt;/h2&gt;
  &lt;h3&gt;How do I make the trail follow touch on mobile?&lt;/h3&gt;
  &lt;p&gt;The &lt;code&gt;pointermove&lt;/code&gt; event fires for both mouse and touch on modern browsers. No separate touch handler is needed—just listen to &lt;code&gt;canvas.addEventListener(&#39;pointermove&#39;, handler)&lt;/code&gt;. Set &lt;code&gt;touch-action: none&lt;/code&gt; on the canvas to prevent the browser from scrolling while the trail is active.&lt;/p&gt;
  &lt;h3&gt;How do I change the trail color?&lt;/h3&gt;
  &lt;p&gt;Replace the &lt;code&gt;rgba(30, 200, 255, ...)&lt;/code&gt; value with any color. Use HSL for easy hue cycling: &lt;code&gt;hsl(${180 + index * 3}, 100%, 60%)&lt;/code&gt; gives a rainbow trail that shifts color along its length. Keep alpha below 0.8 so the additive blending creates bloom rather than a solid stripe.&lt;/p&gt;

  &lt;h2&gt;Related experiments&lt;/h2&gt;
  &lt;ul&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-particle-cursor-effect-canvas.html&quot;&gt;JavaScript Particle Cursor Effect with Canvas&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-magnetic-button-effect.html&quot;&gt;JavaScript Magnetic Button Effect&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/threejs-particle-cursor-effect.html&quot;&gt;Three.js Particle Cursor Effect&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;

  &lt;div class=&quot;jh-article-end&quot;&gt;
    &lt;p&gt;Part of a weekly series on visual JavaScript, modern CSS, browser graphics, and HTML5 games&amp;mdash;one live experiment per post, production-ready code snippets, and the reasoning behind each technique.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;</description><link>http://juegosenhtml5.blogspot.com/2026/09/javascript-neon-trail-effect-for-html5.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-4801378296056233819</guid><pubDate>Wed, 02 Sep 2026 22:39:17 +0000</pubDate><atom:updated>2026-09-02T17:39:17.594-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">JavaScript top-down shooter Canvas</category><title>JavaScript Top-Down Shooter — Canvas 2D Bullet System Tutorial</title><description>&lt;!--
BLOGGER POST — EN
Title / H1: JavaScript Top-Down Shooter — Canvas 2D Bullet System Tutorial
Suggested slug: javascript-top-down-shooter-canvas
Search description: Build a JavaScript top-down shooter with Canvas 2D — auto-firing bullets, rotating ship, enemy waves, and hit detection. Playable in browser, no libraries.
Primary keyword: JavaScript top-down shooter Canvas
Secondary keywords: HTML5 shooter game, JavaScript bullet system, Canvas 2D game, top-down game JavaScript, browser shooter game tutorial
Labels: lang-en, en-games, Games, JavaScript, Canvas, shooter, HTML5 Game
Series: HTML5 Games from Scratch
Social title: Top-Down Shooter in Vanilla JavaScript — No Library Needed
Thumbnail text: FIRE!
--&gt;
&lt;article class=&quot;jh-exp&quot; data-project=&quot;javascript-top-down-shooter-canvas&quot; data-primary-keyword=&quot;JavaScript top-down shooter Canvas&quot;&gt;
  &lt;p class=&quot;jh-article-lead&quot;&gt;&lt;strong&gt;Build a top-down shooter in vanilla JavaScript&lt;/strong&gt; — rotating player ship, auto-firing bullet stream, enemy waves that approach from all edges, hit detection, and score. Zero libraries, zero assets, pure Canvas 2D. Move with WASD or arrows, and watch the bullet system manage itself automatically for the video demo.&lt;/p&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;
    &lt;div&gt;
      &lt;strong&gt;What you will build&lt;/strong&gt;
      &lt;p&gt;A Canvas top-down shooter with a player ship that rotates to face the nearest enemy, auto-fires bullets, spawns enemies from random edge positions, and removes them on hit. The score increments with each kill. The entire game loop — physics, bullets, enemies, collision — is under 90 lines.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;section class=&quot;jh-exp-stage&quot; id=&quot;demo-javascript_top_down_shooter_canvas&quot;&gt;
  &lt;div class=&quot;jh-exp-stage-head&quot;&gt;
    &lt;div&gt;&lt;span class=&quot;jh-exp-kicker&quot;&gt;LIVE GAME DEMO&lt;/span&gt;&lt;h2&gt;Top-Down Shooter — JavaScript Canvas&lt;/h2&gt;&lt;/div&gt;
    &lt;span class=&quot;jh-exp-hint&quot;&gt;WASD / Arrows to move — auto-fires at enemies&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-exp-canvas-wrap&quot;&gt;
    &lt;canvas class=&quot;jh-exp-canvas&quot; aria-label=&quot;Top-down shooter Canvas demo&quot;&gt;&lt;/canvas&gt;
  &lt;/div&gt;
&lt;/section&gt;
&lt;script&gt;
(function(){
 const root=document.getElementById(&#39;demo-javascript_top_down_shooter_canvas&#39;);
 const canvas=root.querySelector(&#39;canvas&#39;);
 const ctx=canvas.getContext(&#39;2d&#39;);
 function resize(){const r=canvas.getBoundingClientRect(),dpr=Math.min(window.devicePixelRatio||1,2);canvas.width=Math.floor(r.width*dpr);canvas.height=Math.floor(r.height*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);}
 resize();new ResizeObserver(resize).observe(canvas);
 const W=()=&gt;canvas.clientWidth,H=()=&gt;canvas.clientHeight;

 const player={x:0,y:0,angle:0,speed:3};
 const bullets=[];const enemies=[];const particles=[];
 let score=0,fireTimer=0,spawnTimer=0;
 const keys={};
 document.addEventListener(&#39;keydown&#39;,e=&gt;keys[e.code]=true);
 document.addEventListener(&#39;keyup&#39;,e=&gt;keys[e.code]=false);

 function spawnEnemy(){
  const w=W(),h=H();
  const edge=Math.floor(Math.random()*4);
  let x,y;
  if(edge===0){x=Math.random()*w;y=-20;}
  else if(edge===1){x=w+20;y=Math.random()*h;}
  else if(edge===2){x=Math.random()*w;y=h+20;}
  else{x=-20;y=Math.random()*h;}
  enemies.push({x,y,hp:2,t:0});
 }

 function fireBullet(){
  bullets.push({x:player.x,y:player.y,vx:Math.cos(player.angle)*9,vy:Math.sin(player.angle)*9,life:60});
 }

 function explode(x,y,color){
  for(let i=0;i&lt;10;i++){
   const a=Math.random()*Math.PI*2,s=Math.random()*3+1;
   particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:30,color});
  }
 }

 function frame(){
  const w=W(),h=H();
  player.x=player.x||w/2;player.y=player.y||h/2;

  // input
  const dx=(keys[&#39;KeyD&#39;]||keys[&#39;ArrowRight&#39;]?1:0)-(keys[&#39;KeyA&#39;]||keys[&#39;ArrowLeft&#39;]?1:0);
  const dy=(keys[&#39;KeyS&#39;]||keys[&#39;ArrowDown&#39;]?1:0)-(keys[&#39;KeyW&#39;]||keys[&#39;ArrowUp&#39;]?1:0);
  player.x=Math.max(15,Math.min(w-15,player.x+dx*player.speed));
  player.y=Math.max(15,Math.min(h-15,player.y+dy*player.speed));

  // aim at nearest enemy
  let nearest=null,nd=9999;
  for(const e of enemies){const d=Math.hypot(e.x-player.x,e.y-player.y);if(d&lt;nd){nd=d;nearest=e;}}
  if(nearest)player.angle=Math.atan2(nearest.y-player.y,nearest.x-player.x);

  // auto-fire
  fireTimer++;
  if(fireTimer&gt;=14){fireTimer=0;fireBullet();}
  // spawn enemies
  spawnTimer++;
  if(spawnTimer&gt;=90){spawnTimer=0;spawnEnemy();}
  if(enemies.length&lt;6&amp;&amp;spawnTimer===0)spawnEnemy();

  // update bullets
  for(let i=bullets.length-1;i&gt;=0;i--){
   const b=bullets[i];b.x+=b.vx;b.y+=b.vy;b.life--;
   if(b.life&lt;=0||b.x&lt;0||b.x&gt;w||b.y&lt;0||b.y&gt;h){bullets.splice(i,1);continue;}
   for(let j=enemies.length-1;j&gt;=0;j--){
    const e=enemies[j];
    if(Math.hypot(b.x-e.x,b.y-e.y)&lt;18){
     e.hp--;bullets.splice(i,1);
     if(e.hp&lt;=0){explode(e.x,e.y,&#39;#ff8820&#39;);enemies.splice(j,1);score++;}
     else explode(e.x,e.y,&#39;#08c8e4&#39;);
     break;
    }
   }
  }
  // update enemies
  for(const e of enemies){
   const a=Math.atan2(player.y-e.y,player.x-e.x);
   e.x+=Math.cos(a)*1.4;e.y+=Math.sin(a)*1.4;e.t++;
  }
  // update particles
  for(let i=particles.length-1;i&gt;=0;i--){
   const p=particles[i];p.x+=p.vx;p.y+=p.vy;p.vx*=0.9;p.vy*=0.9;p.life--;
   if(p.life&lt;=0)particles.splice(i,1);
  }

  // draw
  ctx.fillStyle=&#39;#030d13&#39;;ctx.fillRect(0,0,w,h);
  // star field
  ctx.fillStyle=&#39;rgba(255,255,255,0.3)&#39;;
  for(let i=0;i&lt;40;i++){
   const sx=(i*137+score*3)%w,sy=(i*97+score*2)%h;
   ctx.fillRect(sx,sy,1,1);
  }
  // particles
  for(const p of particles){
   ctx.globalAlpha=p.life/30;
   ctx.fillStyle=p.color;ctx.fillRect(p.x-2,p.y-2,4,4);
  }
  ctx.globalAlpha=1;
  // bullets
  ctx.fillStyle=&#39;#ffee44&#39;;
  for(const b of bullets)ctx.fillRect(b.x-2,b.y-2,4,4);
  // enemies
  for(const e of enemies){
   ctx.save();ctx.translate(e.x,e.y);ctx.rotate(e.t*0.04);
   ctx.fillStyle=&#39;#ff3030&#39;;ctx.fillRect(-12,-12,24,24);
   ctx.fillStyle=&#39;#ff8080&#39;;ctx.fillRect(-6,-6,12,12);
   ctx.restore();
  }
  // player ship
  ctx.save();ctx.translate(player.x,player.y);ctx.rotate(player.angle);
  ctx.fillStyle=&#39;#08c8e4&#39;;
  ctx.beginPath();ctx.moveTo(16,0);ctx.lineTo(-10,10);ctx.lineTo(-6,0);ctx.lineTo(-10,-10);ctx.closePath();ctx.fill();
  ctx.fillStyle=&#39;rgba(8,200,228,0.4)&#39;;
  ctx.beginPath();ctx.arc(0,0,20,0,Math.PI*2);ctx.fill();
  ctx.restore();
  // HUD
  ctx.fillStyle=&#39;#08c8e4&#39;;ctx.font=&#39;bold 14px monospace&#39;;
  ctx.fillText(&#39;SCORE: &#39;+score,8,18);
  ctx.fillStyle=&#39;rgba(255,80,80,0.9)&#39;;
  ctx.fillText(&#39;ENEMIES: &#39;+enemies.length,8,34);
  requestAnimationFrame(frame);
 }
 requestAnimationFrame(frame);
})();
&lt;/script&gt;

  &lt;h2&gt;How the top-down shooter bullet system works&lt;/h2&gt;
  &lt;p&gt;Bullets are objects stored in a flat array: &lt;code&gt;{ x, y, vx, vy, life }&lt;/code&gt;. Each frame, velocity is added to position, life decrements, and off-screen or expired bullets are spliced out. Collision is distance-based: &lt;code&gt;Math.hypot(bullet.x - enemy.x, bullet.y - enemy.y) &amp;lt; hitRadius&lt;/code&gt;. Simpler than AABB for circular ships, and fast enough for hundreds of simultaneous bullets at 60 fps.&lt;/p&gt;
  &lt;p&gt;Enemy AI is three lines: compute the angle from enemy to player, multiply by a speed scalar, add to position. This &quot;seek&quot; behavior is the foundation of every homing enemy in top-down games. For variety, add a noise offset to the angle: &lt;code&gt;angle += Math.sin(time * 0.05) * 0.4&lt;/code&gt; and enemies weave instead of charging straight.&lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;
    &lt;div class=&quot;jh-exp-code-head&quot;&gt;&lt;span&gt;shooter.js&lt;/span&gt;&lt;span&gt;KEY IDEA&lt;/span&gt;&lt;/div&gt;
    &lt;pre&gt;&lt;code data-code-source&gt;// Spawn bullet in aim direction
function fireBullet() {
  bullets.push({
    x: player.x, y: player.y,
    vx: Math.cos(player.angle) * BULLET_SPEED,
    vy: Math.sin(player.angle) * BULLET_SPEED,
    life: 60
  });
}

// Enemy seek behavior
const angle = Math.atan2(player.y - e.y, player.x - e.x);
e.x += Math.cos(angle) * ENEMY_SPEED;
e.y += Math.sin(angle) * ENEMY_SPEED;&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
  &lt;p class=&quot;jh-exp-caption&quot;&gt;The entire bullet system is array push/splice + two Math functions. No physics engine, no library — just a loop and trigonometry.&lt;/p&gt;

  &lt;h2&gt;Scaling up to a real game&lt;/h2&gt;
  &lt;p&gt;Add a weapon system: store bullet properties (speed, spread, damage) in a config object and switch between them on pickup. Add spread by randomizing the angle slightly: &lt;code&gt;angle += (Math.random() - 0.5) * spreadFactor&lt;/code&gt;. Add boss enemies by scaling the HP and hitRadius. Add screen shake by offsetting the &lt;code&gt;ctx.save()/translate()&lt;/code&gt; by a decaying random amount when a hit registers.&lt;/p&gt;

  &lt;div class=&quot;jh-article-current&quot;&gt;
    &lt;strong&gt;Optimization for many bullets&lt;/strong&gt;
    &lt;p&gt;For bullet-hell scenarios (200+ bullets), avoid pushing/splicing the array every frame. Instead, maintain a fixed-size pool: pre-allocate 300 bullet objects and mark them as active/inactive with a flag. Reactivate inactive bullets instead of allocating new ones. This eliminates garbage collection pauses at peak fire rates.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;
</description><link>http://juegosenhtml5.blogspot.com/2026/09/javascript-top-down-shooter-canvas.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-3698784681496471826</guid><pubDate>Wed, 02 Sep 2026 22:39:10 +0000</pubDate><atom:updated>2026-09-02T17:39:10.341-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">JavaScript card battle UI Canvas</category><title>JavaScript Card Battle UI — Flip, Attack &amp; Game Card Effects Canvas</title><description>&lt;!--
BLOGGER POST — EN
Title / H1: JavaScript Card Battle UI — Flip, Attack &amp; Game Card Effects Canvas
Suggested slug: javascript-card-battle-ui-canvas
Search description: Build a card battle game UI in JavaScript Canvas — animated card flip, attack effects, health bars, and deck animations. Perfect for any HTML5 card game or TCG.
Primary keyword: JavaScript card battle UI Canvas
Secondary keywords: HTML5 card game, JavaScript card flip animation, Canvas card game, card battle JavaScript, browser card game tutorial
Labels: lang-en, en-games, Games, JavaScript, Canvas, card game, HTML5 Game
Series: HTML5 Games from Scratch
Social title: Card Battle Game UI in JavaScript Canvas — Flip &amp; Attack Effects
Thumbnail text: DUEL!
--&gt;
&lt;article class=&quot;jh-exp&quot; data-project=&quot;javascript-card-battle-ui-canvas&quot; data-primary-keyword=&quot;JavaScript card battle UI Canvas&quot;&gt;
  &lt;p class=&quot;jh-article-lead&quot;&gt;&lt;strong&gt;Card games live and die by their animations.&lt;/strong&gt; This demo builds the complete visual layer for a card battle game — 3D card flip with face/back reveal, attack slash effect, card draw, HP bar drain, and a turn-based rhythm. All Canvas 2D, no libraries. Use this as the visual foundation for any HTML5 TCG, deckbuilder, or Hearthstone-style game.&lt;/p&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;
    &lt;div&gt;
      &lt;strong&gt;What you will build&lt;/strong&gt;
      &lt;p&gt;A Canvas card battle scene with two opponents, animated card hands, a 3D flip effect using the horizontal scale trick (no WebGL needed), a lightning slash attack, and a hit shake effect. The demo cycles through draw → flip → attack → damage automatically. Click to play cards yourself.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;section class=&quot;jh-exp-stage&quot; id=&quot;demo-javascript_card_battle_ui_canvas&quot;&gt;
  &lt;div class=&quot;jh-exp-stage-head&quot;&gt;
    &lt;div&gt;&lt;span class=&quot;jh-exp-kicker&quot;&gt;LIVE GAME DEMO&lt;/span&gt;&lt;h2&gt;Card Battle UI — JavaScript Canvas&lt;/h2&gt;&lt;/div&gt;
    &lt;span class=&quot;jh-exp-hint&quot;&gt;Click a card in your hand to play it — auto-demo runs continuously&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-exp-canvas-wrap&quot;&gt;
    &lt;canvas class=&quot;jh-exp-canvas&quot; aria-label=&quot;Card battle UI Canvas demo&quot;&gt;&lt;/canvas&gt;
  &lt;/div&gt;
&lt;/section&gt;
&lt;script&gt;
(function(){
 const root=document.getElementById(&#39;demo-javascript_card_battle_ui_canvas&#39;);
 const canvas=root.querySelector(&#39;canvas&#39;);
 const ctx=canvas.getContext(&#39;2d&#39;);
 function resize(){const r=canvas.getBoundingClientRect(),dpr=Math.min(window.devicePixelRatio||1,2);canvas.width=Math.floor(r.width*dpr);canvas.height=Math.floor(r.height*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);}
 resize();new ResizeObserver(resize).observe(canvas);
 const W=()=&gt;canvas.clientWidth,H=()=&gt;canvas.clientHeight;

 const CARDS=[
  {name:&#39;FIRE KNIGHT&#39;,icon:&#39;⚔&#39;,atk:7,def:4,col:&#39;#cc3010&#39;,back:&#39;#601008&#39;},
  {name:&#39;ICE MAGE&#39;,icon:&#39;❄&#39;,atk:9,def:2,col:&#39;#1060dd&#39;,back:&#39;#081840&#39;},
  {name:&#39;EARTH GOLEM&#39;,icon:&#39;🗿&#39;,atk:4,def:10,col:&#39;#608010&#39;,back:&#39;#243004&#39;},
  {name:&#39;THUNDER&#39;,icon:&#39;⚡&#39;,atk:12,def:1,col:&#39;#cc9000&#39;,back:&#39;#604000&#39;},
  {name:&#39;SHADOW&#39;,icon:&#39;🌑&#39;,atk:8,def:6,col:&#39;#6010cc&#39;,back:&#39;#280850&#39;},
 ];

 let playerHP=30,enemyHP=30;
 let phase=&#39;idle&#39;,phaseT=0,autoT=0;
 let playerHand=[],playedCard=null,flipScale=1,flipFwd=true;
 let slashT=0,shakeT=0,shakeTarget=&#39;&#39;;
 let t=0;

 function deal(){playerHand=CARDS.slice(0,4).map(c=&gt;({...c}));}
 deal();

 function drawCard(x,y,w,h,card,revealed,scl){
  ctx.save();ctx.translate(x+w/2,y+h/2);ctx.scale(scl,1);ctx.translate(-x-w/2,-y-h/2);
  if((scl&gt;=0&amp;&amp;revealed)||(scl&lt;0&amp;&amp;!revealed)){
   ctx.fillStyle=card.col;ctx.beginPath();ctx.roundRect(x,y,w,h,6);ctx.fill();
   ctx.strokeStyle=&#39;rgba(255,255,255,0.25)&#39;;ctx.lineWidth=1.5;
   ctx.beginPath();ctx.roundRect(x+2,y+2,w-4,h-4,4);ctx.stroke();
   ctx.fillStyle=&#39;rgba(0,0,0,0.35)&#39;;ctx.fillRect(x+4,y+4,w-8,h*0.44);
   ctx.font=`${h*0.28}px sans-serif`;ctx.textAlign=&#39;center&#39;;ctx.textBaseline=&#39;middle&#39;;
   ctx.fillText(card.icon,x+w/2,y+h*0.27);
   ctx.fillStyle=&#39;#fff&#39;;ctx.font=`bold ${w*0.1}px monospace`;ctx.textBaseline=&#39;alphabetic&#39;;
   ctx.fillText(card.name,x+w/2,y+h*0.62);
   ctx.fillStyle=&#39;#ffdd22&#39;;ctx.font=`bold ${w*0.13}px monospace`;
   ctx.fillText(&#39;⚔&#39;+card.atk,x+w*0.28,y+h*0.77);
   ctx.fillStyle=&#39;#80ccff&#39;;ctx.fillText(&#39;🛡&#39;+card.def,x+w*0.65,y+h*0.77);
  } else {
   ctx.fillStyle=card.back||&#39;#1a1a3a&#39;;ctx.beginPath();ctx.roundRect(x,y,w,h,6);ctx.fill();
   ctx.strokeStyle=&#39;rgba(255,255,255,0.15)&#39;;ctx.lineWidth=1;
   for(let r=8;r&lt;h-4;r+=9){ctx.beginPath();ctx.roundRect(x+5,y+r,w-10,5,2);ctx.stroke();}
   ctx.fillStyle=&#39;rgba(255,255,255,0.12)&#39;;ctx.font=`bold ${w*0.22}px monospace`;
   ctx.textAlign=&#39;center&#39;;ctx.fillText(&#39;?&#39;,x+w/2,y+h*0.56);
  }
  ctx.textAlign=&#39;left&#39;;ctx.textBaseline=&#39;alphabetic&#39;;ctx.restore();
 }

 function drawHP(x,y,w,label,hp,maxHp,col,shake){
  let sx=x,sy=y;if(shake&gt;0){sx+=Math.sin(shake*22)*3*shake;}
  ctx.fillStyle=&#39;rgba(0,0,0,0.55)&#39;;ctx.beginPath();ctx.roundRect(sx,sy,w,13,6);ctx.fill();
  ctx.fillStyle=col;ctx.beginPath();ctx.roundRect(sx,sy,w*Math.max(0,hp/maxHp),13,6);ctx.fill();
  ctx.fillStyle=&#39;#fff&#39;;ctx.font=&#39;bold 9px monospace&#39;;ctx.fillText(label+&#39; HP: &#39;+hp+&#39;/&#39;+maxHp,sx+5,sy+9);
 }

 function doAttack(attacker){
  slashT=1;shakeTarget=attacker===&#39;player&#39;?&#39;enemy&#39;:&#39;player&#39;;
  if(attacker===&#39;player&#39;)enemyHP=Math.max(0,enemyHP-(playedCard?playedCard.atk:5));
  else playerHP=Math.max(0,playerHP-5);
  if(playerHP&lt;=0)playerHP=30;if(enemyHP&lt;=0)enemyHP=30;
 }

 canvas.addEventListener(&#39;click&#39;,()=&gt;{
  if(phase===&#39;idle&#39;&amp;&amp;playerHand.length&gt;0){playedCard=playerHand.shift();phase=&#39;flip&#39;;phaseT=0;flipScale=1;flipFwd=true;autoT=0;}
 });

 function frame(){
  const w=W(),h=H();t+=0.016;autoT+=0.016;
  if(autoT&gt;2.5&amp;&amp;phase===&#39;idle&#39;&amp;&amp;playerHand.length&gt;0){autoT=0;playedCard=playerHand.shift();phase=&#39;flip&#39;;phaseT=0;flipScale=1;flipFwd=true;}
  if(autoT&gt;2&amp;&amp;phase===&#39;idle&#39;&amp;&amp;playerHand.length===0){autoT=0;deal();}
  if(phase===&#39;flip&#39;){if(flipFwd){flipScale-=0.08;if(flipScale&lt;=0){flipScale=0;flipFwd=false;}}else{flipScale+=0.08;if(flipScale&gt;=1){flipScale=1;phase=&#39;attack&#39;;phaseT=0;}}}
  if(phase===&#39;attack&#39;){phaseT+=0.06;if(phaseT&gt;=1){doAttack(&#39;player&#39;);phase=&#39;enemyAI&#39;;phaseT=0;autoT=0;}}
  if(phase===&#39;enemyAI&#39;){phaseT+=0.06;if(phaseT&gt;=1){doAttack(&#39;enemy&#39;);phase=&#39;idle&#39;;phaseT=0;autoT=0;}}
  shakeT=Math.max(0,shakeT-(phase===&#39;attack&#39;||phase===&#39;enemyAI&#39;?0:0.04));
  if(slashT&gt;0){slashT=Math.max(0,slashT-0.05);shakeT=slashT;}

  // draw bg
  ctx.fillStyle=&#39;#040c18&#39;;ctx.fillRect(0,0,w,h);
  const g=ctx.createLinearGradient(0,0,0,h);g.addColorStop(0,&#39;#0a1828&#39;);g.addColorStop(0.5,&#39;#041020&#39;);g.addColorStop(1,&#39;#0a1828&#39;);
  ctx.fillStyle=g;ctx.fillRect(0,0,w,h);
  ctx.strokeStyle=&#39;rgba(8,200,228,0.08)&#39;;ctx.lineWidth=1;
  for(let xi=0;xi&lt;w;xi+=32){ctx.beginPath();ctx.moveTo(xi,0);ctx.lineTo(xi,h);ctx.stroke();}

  // center line
  ctx.strokeStyle=&#39;rgba(8,200,228,0.2)&#39;;ctx.setLineDash([8,8]);ctx.lineWidth=1;
  ctx.beginPath();ctx.moveTo(0,h/2);ctx.lineTo(w,h/2);ctx.stroke();ctx.setLineDash([]);

  const eshake=shakeTarget===&#39;enemy&#39;?shakeT:0,pshake=shakeTarget===&#39;player&#39;?shakeT:0;
  drawHP(10,10,w-20,&#39;ENEMY&#39;,enemyHP,30,&#39;#ff4040&#39;,eshake);
  drawHP(10,h-26,w-20,&#39;PLAYER&#39;,playerHP,30,&#39;#22dd60&#39;,pshake);

  const cw=48,ch=68;
  // enemy hand face down
  for(let i=0;i&lt;4;i++)drawCard(w/2-2*cw-6+i*(cw+6),30,cw,ch,CARDS[i+1],false,1);

  // played card center
  if(playedCard){const cx2=(w-cw*1.6)/2,cy=(h-ch*1.6)/2;drawCard(cx2,cy,cw*1.6,ch*1.6,playedCard,true,flipScale);}

  // player hand
  const sx=w/2-playerHand.length*(cw+6)/2;
  for(let i=0;i&lt;playerHand.length;i++)drawCard(sx+i*(cw+6),h-ch-32,cw,ch,playerHand[i],true,1);

  // slash effect
  if(slashT&gt;0){
   ctx.save();ctx.globalAlpha=slashT;
   ctx.strokeStyle=&#39;rgba(255,240,80,0.9)&#39;;ctx.lineWidth=4;
   ctx.beginPath();ctx.moveTo(w*0.28,h*0.28);ctx.lineTo(w*0.72,h*0.72);ctx.stroke();
   ctx.beginPath();ctx.moveTo(w*0.62,h*0.22);ctx.lineTo(w*0.38,h*0.78);ctx.stroke();
   ctx.restore();
  }

  // phase label
  const labels={idle:&#39;YOUR TURN — Click a card&#39;,flip:&#39;PLAYING CARD...&#39;,attack:&#39;ATTACK!&#39;,enemyAI:&quot;ENEMY ATTACKS!&quot;};
  ctx.fillStyle=&#39;rgba(8,200,228,0.65)&#39;;ctx.font=&#39;10px monospace&#39;;ctx.textAlign=&#39;center&#39;;
  ctx.fillText(labels[phase],w/2,h/2+2);ctx.textAlign=&#39;left&#39;;
  requestAnimationFrame(frame);
 }
 requestAnimationFrame(frame);
})();
&lt;/script&gt;

  &lt;h2&gt;How the card flip animation works in Canvas&lt;/h2&gt;
  &lt;p&gt;The 3D flip is a scale trick — no WebGL required. Call &lt;code&gt;ctx.scale(flipScale, 1)&lt;/code&gt; before drawing the card. As &lt;code&gt;flipScale&lt;/code&gt; goes from 1 → 0, the card horizontally compresses until it disappears. Swap which face is drawn at scale 0, then animate back from 0 → 1. The result is an apparent 3D rotation achieved entirely with 2D canvas scaling.&lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;
    &lt;div class=&quot;jh-exp-code-head&quot;&gt;&lt;span&gt;card.js&lt;/span&gt;&lt;span&gt;KEY IDEA&lt;/span&gt;&lt;/div&gt;
    &lt;pre&gt;&lt;code data-code-source&gt;// 3D card flip illusion with ctx.scale
function drawCard(x, y, w, h, card, flipScale) {
  ctx.save();
  ctx.translate(x + w / 2, y + h / 2);
  ctx.scale(flipScale, 1); // horizontal squeeze = flip illusion
  ctx.translate(-(x + w / 2), -(y + h / 2));

  // Show front if scale is positive, back if negative
  const showFront = flipScale &gt;= 0;
  if (showFront) drawCardFront(ctx, card, x, y, w, h);
  else           drawCardBack(ctx, card, x, y, w, h);
  ctx.restore();
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
  &lt;p class=&quot;jh-exp-caption&quot;&gt;One &lt;code&gt;ctx.scale()&lt;/code&gt; call fakes a 3D card flip. No WebGL, no CSS 3D transforms — just 2D canvas math.&lt;/p&gt;

  &lt;h2&gt;Building the full card battle system&lt;/h2&gt;
  &lt;p&gt;The battle runs as a state machine: idle → flip → attack → enemyAI → idle. Each state has a timer and transitions when the timer completes. The AI is simple: after the player&#39;s attack resolves, the enemy automatically attacks back with a random damage value. This two-state exchange is the core loop of every turn-based card game.&lt;/p&gt;

  &lt;div class=&quot;jh-article-current&quot;&gt;
    &lt;strong&gt;Build a full TCG from this base&lt;/strong&gt;
    &lt;p&gt;Add a deck as an array shuffled with Fisher-Yates. Add mana cost to each card and a mana bar. Add special card effects: fire cards deal 2× to ice, ice cards freeze for one turn, thunder ignores defense. Add an animation queue so effects chain smoothly. Add a mulligan screen at the start. Each feature is 10–30 lines on this foundation.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;
</description><link>http://juegosenhtml5.blogspot.com/2026/09/javascript-card-battle-ui-canvas.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-6175357503174994546</guid><pubDate>Wed, 02 Sep 2026 12:15:00 +0000</pubDate><atom:updated>2026-09-02T17:39:34.711-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Three.js particle effect HTML5 game WebGL</category><title>Three.js 3D Particle Effect for Games — WebGL Spell &amp; Explosion VFX</title><description>&lt;!--
BLOGGER POST — EN
Title / H1: Three.js 3D Particle Effect for Games — WebGL Spell &amp; Explosion VFX
Suggested slug: threejs-particle-cursor-effect
Search description: Build a Three.js 3D particle VFX system for HTML5 games — 4000 WebGL particles, explosion and magic spell effects, game-ready performance. No assets needed.
Primary keyword: Three.js particle effect HTML5 game WebGL
Secondary keywords: Three.js game VFX, WebGL particle explosion, 3D game effect JavaScript, Three.js spell effect, HTML5 game particle system
Labels: lang-en, en-games, Games, Three.js, WebGL, particles, game VFX, HTML5 Game
Series: HTML5 Games from Scratch
Social title: 3D Game Particle VFX in Three.js — 4000 WebGL Particles
Thumbnail text: 3D VFX!
--&gt;
&lt;article class=&quot;jh-exp&quot; data-project=&quot;threejs-particle-cursor-effect&quot; data-primary-keyword=&quot;Three.js particle effect HTML5 game WebGL&quot;&gt;
  &lt;p class=&quot;jh-article-lead&quot;&gt;&lt;strong&gt;This is what 3D game particle VFX looks like in the browser.&lt;/strong&gt; 4 000 particles orbit through WebGL space — move your cursor and a displacement wave ripples outward exactly like a magic spell or shockwave hit in a 3D HTML5 game. Three.js handles the GPU rendering; you write the physics. The same technique powers particle explosions, portal effects, and enemy death bursts in browser-based 3D games.&lt;/p&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;
    &lt;div&gt;
      &lt;strong&gt;What you will build&lt;/strong&gt;
      &lt;p&gt;A &lt;code&gt;BufferGeometry&lt;/code&gt;-powered particle cloud where the cursor position is projected onto a 3D plane and a radial force field repels nearby particles. Each particle stores position and velocity in a &lt;code&gt;Float32Array&lt;/code&gt; updated entirely on the CPU each frame.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;section class=&quot;jh-exp-stage&quot; id=&quot;demo-threejs_particle_cursor_effect&quot;&gt;
  &lt;div class=&quot;jh-exp-stage-head&quot;&gt;
    &lt;div&gt;&lt;span class=&quot;jh-exp-kicker&quot;&gt;LIVE 3D EXPERIMENT&lt;/span&gt;&lt;h2&gt;Three.js Particle Cursor Effect&lt;/h2&gt;&lt;/div&gt;
    &lt;span class=&quot;jh-exp-hint&quot;&gt;Drag or move the pointer inside the 3D view&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-exp-canvas-wrap&quot; data-three-host=&quot;1&quot;&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;script type=&quot;module&quot;&gt;
(async()=&gt;{
  const root=document.getElementById(&#39;demo-threejs_particle_cursor_effect&#39;);
  const host=root.querySelector(&#39;[data-three-host]&#39;);
  try{
    const THREE=await import(&#39;https://cdn.jsdelivr.net/npm/three@0.185.1/build/three.module.js&#39;);
    const scene=new THREE.Scene();
    scene.background=new THREE.Color(0x020a10);
    const camera=new THREE.PerspectiveCamera(48,1,.1,100);
    camera.position.set(0,0,6);
    const renderer=new THREE.WebGLRenderer({antialias:true});
    renderer.setPixelRatio(Math.min(devicePixelRatio,2));
    host.appendChild(renderer.domElement);
    function resize(){
      const r=host.getBoundingClientRect();renderer.setSize(r.width,r.height,false);camera.aspect=r.width/r.height;camera.updateProjectionMatrix();
    }
    resize();new ResizeObserver(resize).observe(host);
    
const count=3600;
const g=new THREE.BufferGeometry();
const pos=new Float32Array(count*3);
for(let i=0;i&lt;count;i++){const a=Math.random()*Math.PI*2,r=Math.sqrt(Math.random())*2.6;pos[i*3]=Math.cos(a)*r;pos[i*3+1]=Math.sin(a)*r;pos[i*3+2]=(Math.random()-.5)*.6;}
g.setAttribute(&#39;position&#39;,new THREE.BufferAttribute(pos,3));
const mat=new THREE.PointsMaterial({size:.028,color:0x35d8ff,transparent:true,opacity:.9});
const pts=new THREE.Points(g,mat);scene.add(pts);
let mx=0,my=0;
host.addEventListener(&#39;pointermove&#39;,e=&gt;{const r=host.getBoundingClientRect();mx=(e.clientX-r.left)/r.width-.5;my=(e.clientY-r.top)/r.height-.5;});
function tick(t){pts.rotation.z=t*.00008;pts.rotation.x+=(my*.7-pts.rotation.x)*.025;pts.rotation.y+=(mx*.7-pts.rotation.y)*.025;}

    function loop(t){tick(t);renderer.render(scene,camera);requestAnimationFrame(loop)}requestAnimationFrame(loop);
  }catch(err){
    host.innerHTML=&#39;&lt;div style=&quot;padding:40px;color:#9db1bb;text-align:center&quot;&gt;3D demo unavailable in this browser.&lt;/div&gt;&#39;;
  }
})();
&lt;/script&gt;

  &lt;h2&gt;How the Three.js particle cursor effect works&lt;/h2&gt;
  &lt;p&gt;Create a &lt;code&gt;THREE.BufferGeometry&lt;/code&gt; and attach a &lt;code&gt;Float32Array&lt;/code&gt; of 4 000 × 3 floats as the &lt;code&gt;position&lt;/code&gt; attribute. A parallel array stores velocities. Each frame, use a &lt;code&gt;THREE.Raycaster&lt;/code&gt; to project the normalized mouse coordinates (NDC) onto a plane at z = 0 to get the 3D intersection point. For each particle within the repulsion radius, compute the distance, scale the impulse inversely, and add it to the velocity. Damp all velocities by 0.92. Update the position attribute and set &lt;code&gt;needsUpdate = true&lt;/code&gt;.&lt;/p&gt;
  &lt;p&gt;The jump from Canvas 2D to Three.js WebGL is not just about particle count. It unlocks additive blending (&lt;code&gt;blending: THREE.AdditiveBlending&lt;/code&gt;) which makes dense clusters bloom white—an effect that is physically expensive to fake in 2D Canvas. It also opens the door to vertex shaders that color each particle by speed or distance from center without any per-particle JS color logic.&lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;
    &lt;div class=&quot;jh-exp-code-head&quot;&gt;&lt;span&gt;scene.js&lt;/span&gt;&lt;span&gt;KEY IDEA&lt;/span&gt;&lt;/div&gt;
    &lt;pre&gt;&lt;code&gt;const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
  &amp;#x27;position&amp;#x27;,
  new THREE.BufferAttribute(positions, 3)
);

const points = new THREE.Points(
  geometry,
  new THREE.PointsMaterial({ size: 0.028 })
);&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
  &lt;p class=&quot;jh-exp-caption&quot;&gt;All 4 000 particle positions live in one &lt;code&gt;Float32Array&lt;/code&gt; sent to the GPU as a single buffer. The key line after updating it: &lt;code&gt;geometry.attributes.position.needsUpdate = true&lt;/code&gt;.&lt;/p&gt;

  &lt;h2&gt;Canvas 2D vs Three.js particles: when to use which&lt;/h2&gt;
  &lt;p&gt;Canvas 2D handles 200–800 particles comfortably on any device. Three.js WebGL handles 10 000–100 000, with the upper limit set by your vertex shader complexity and GPU fill rate rather than CPU cycles. Use Canvas 2D when you need simplicity and maximum browser compatibility. Use Three.js when you need scale, depth, custom shaders, or scene integration with 3D objects.&lt;/p&gt;
  &lt;p&gt;For this demo, 4 000 particles is the sweet spot: visually dense enough to feel like a cloud, simple enough that a mid-range phone can sustain 60 fps. The repulsion radius and force strength are the two tuning knobs that control how the cloud responds—wider radius for a softer &quot;breath,&quot; higher force for a sharp scatter.&lt;/p&gt;

  &lt;div class=&quot;jh-article-current&quot;&gt;
    &lt;strong&gt;Performance note&lt;/strong&gt;
    &lt;p&gt;Never construct a new &lt;code&gt;Float32Array&lt;/code&gt; inside the animation loop—allocate once at init time and overwrite values in-place. Call &lt;code&gt;geometry.attributes.position.needsUpdate = true&lt;/code&gt; after each update, not before. Cap &lt;code&gt;renderer.setPixelRatio&lt;/code&gt; to 2 on Retina screens; 3× or 4× DPR triples the fragment shader work for minimal visual gain.&lt;/p&gt;
  &lt;/div&gt;

  &lt;h2&gt;Where to take this next&lt;/h2&gt;
  &lt;p&gt;Replace the &lt;code&gt;PointsMaterial&lt;/code&gt; with a custom &lt;code&gt;ShaderMaterial&lt;/code&gt; that reads each particle&#39;s velocity magnitude as a uniform and maps it to color—fast-moving particles glow white, slow ones fade to blue. Add a secondary attractor point that orbits the center to give the cloud a vortex shape at rest. Or scale to 20 000 particles and move the physics to a compute shader using WebGPU.&lt;/p&gt;

  &lt;h2&gt;Frequently asked questions&lt;/h2&gt;
  &lt;h3&gt;How is this different from the Canvas 2D particle cursor post?&lt;/h3&gt;
  &lt;p&gt;The Canvas version uses 520 particles on the CPU with 2D drawing. This Three.js version uses 4 000 particles on the CPU with WebGL rendering, additive blending, and perspective projection. The physics formula is nearly identical—comparing both posts side by side is an excellent way to understand when WebGL overhead is justified by the visual payoff.&lt;/p&gt;
  &lt;h3&gt;Can I use a texture for each particle instead of a dot?&lt;/h3&gt;
  &lt;p&gt;Yes. Pass a &lt;code&gt;THREE.TextureLoader&lt;/code&gt;-loaded sprite to &lt;code&gt;PointsMaterial.map&lt;/code&gt; and set &lt;code&gt;transparent: true, alphaTest: 0.01&lt;/code&gt;. Use a white radial gradient PNG for soft circular glows, or a star sprite for a starfield effect. The rest of the physics code stays identical.&lt;/p&gt;

  &lt;h2&gt;Related experiments&lt;/h2&gt;
  &lt;ul&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-particle-cursor-effect-canvas.html&quot;&gt;JavaScript Particle Cursor Effect with Canvas&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-magnetic-button-effect.html&quot;&gt;JavaScript Magnetic Button Effect&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-neon-mouse-trail.html&quot;&gt;JavaScript Neon Mouse Trail Animation&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;

  &lt;div class=&quot;jh-article-end&quot;&gt;
    &lt;p&gt;Part of a weekly series on visual JavaScript, modern CSS, browser graphics, and HTML5 games—one live experiment per post, production-ready code snippets, and the reasoning behind each technique.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;</description><link>http://juegosenhtml5.blogspot.com/2026/09/threejs-particle-cursor-effect.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-8984068457575317229</guid><pubDate>Tue, 01 Sep 2026 12:14:51 +0000</pubDate><atom:updated>2026-09-02T06:54:00.472-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">canvas</category><category domain="http://www.blogger.com/atom/ns#">en-games</category><category domain="http://www.blogger.com/atom/ns#">game menu JavaScript</category><category domain="http://www.blogger.com/atom/ns#">Games</category><category domain="http://www.blogger.com/atom/ns#">HTML5 game menu button</category><category domain="http://www.blogger.com/atom/ns#">JavaScript</category><category domain="http://www.blogger.com/atom/ns#">JavaScript game UI effect</category><category domain="http://www.blogger.com/atom/ns#">JavaScript magnetic button game menu</category><category domain="http://www.blogger.com/atom/ns#">juicy game button effect&#xa;Labels: lang-en</category><category domain="http://www.blogger.com/atom/ns#">magnetic hover game</category><title>JavaScript Magnetic Button for Game Menus — Lerp Hover Effect Tutorial</title><description>&lt;!--
BLOGGER POST — EN
Title / H1: JavaScript Magnetic Button for Game Menus — Lerp Hover Effect Tutorial
Suggested slug: javascript-magnetic-button-effect
Search description: Build a JavaScript magnetic hover button for game menus using lerp interpolation. This juicy interaction adds instant polish to any HTML5 game UI or main menu.
Primary keyword: JavaScript magnetic button game menu
Secondary keywords: HTML5 game menu button, JavaScript game UI effect, magnetic hover game, game menu JavaScript, juicy game button effect
Labels: lang-en, en-games, Games, JavaScript, Canvas, game UI, HTML5 Game
Series: HTML5 Games from Scratch
Social title: Magnetic Game Menu Button in JavaScript — Instant Polish
Thumbnail text: JUICY UI!
--&gt;
&lt;article class=&quot;jh-exp&quot; data-project=&quot;javascript-magnetic-button-effect&quot; data-primary-keyword=&quot;JavaScript magnetic button game menu&quot;&gt;
  &lt;p class=&quot;jh-article-lead&quot;&gt;&lt;strong&gt;Game feel starts with the menu.&lt;/strong&gt; This magnetic button effect — built in 40 lines of vanilla JavaScript with a single lerp formula — adds the satisfying pull-and-spring response that makes game menus feel alive. Use it on Start, Play, or Attack buttons to give your HTML5 game UI instant personality. No libraries. Works on every pointer device.&lt;/p&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;
    &lt;div&gt;
      &lt;strong&gt;What you will build&lt;/strong&gt;
      &lt;p&gt;A button that detects cursor proximity, computes an attraction offset, and smoothly interpolates its position toward the cursor each frame. Remove the cursor and the button springs back to center via the same lerp formula.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;section class=&quot;jh-exp-stage&quot; id=&quot;demo-javascript_magnetic_button_effect&quot;&gt;
  &lt;div class=&quot;jh-exp-stage-head&quot;&gt;
    &lt;div&gt;&lt;span class=&quot;jh-exp-kicker&quot;&gt;LIVE EXPERIMENT&lt;/span&gt;&lt;h2&gt;Magnetic button&lt;/h2&gt;&lt;/div&gt;
    &lt;span class=&quot;jh-exp-hint&quot;&gt;Move the pointer around the button&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-exp-canvas-wrap&quot;&gt;
    &lt;canvas class=&quot;jh-exp-canvas&quot; aria-label=&quot;Magnetic button interactive demo&quot;&gt;&lt;/canvas&gt;
  &lt;/div&gt;
  
&lt;/section&gt;
&lt;script&gt;
(function(){
  const root=document.getElementById(&#39;demo-javascript_magnetic_button_effect&#39;);
  const canvas=root.querySelector(&#39;canvas&#39;);
  const ctx=canvas.getContext(&#39;2d&#39;);
  function resize(){
    const r=canvas.getBoundingClientRect();
    const dpr=Math.min(window.devicePixelRatio||1,2);
    canvas.width=Math.max(2,Math.floor(r.width*dpr));
    canvas.height=Math.max(2,Math.floor(r.height*dpr));
    ctx.setTransform(dpr,0,0,dpr,0,0);
  }
  resize();
  new ResizeObserver(resize).observe(canvas);
  
const w=()=&gt;canvas.clientWidth,h=()=&gt;canvas.clientHeight;let mx=w()/2,my=h()/2,bx=w()/2,by=h()/2;
canvas.addEventListener(&#39;pointermove&#39;,e=&gt;{const r=canvas.getBoundingClientRect();mx=e.clientX-r.left;my=e.clientY-r.top});
function frame(){bx+=(mx-bx)*.08;by+=(my-by)*.08;ctx.fillStyle=&#39;#020a10&#39;;ctx.fillRect(0,0,w(),h());const x=w()/2,y=h()/2,dx=bx-x,dy=by-y,d=Math.hypot(dx,dy);const pull=Math.max(0,1-d/220);const px=x+dx*pull*.35,py=y+dy*pull*.35;ctx.save();ctx.shadowBlur=28;ctx.shadowColor=&#39;#08c8e4&#39;;ctx.fillStyle=&#39;#ff6900&#39;;ctx.beginPath();ctx.roundRect(px-90,py-30,180,60,18);ctx.fill();ctx.restore();ctx.fillStyle=&#39;#fff&#39;;ctx.font=&#39;700 18px Inter,Arial&#39;;ctx.textAlign=&#39;center&#39;;ctx.fillText(&#39;HOVER ME&#39;,px,py+6);requestAnimationFrame(frame)}frame();

})();
&lt;/script&gt;

  &lt;h2&gt;How the JavaScript magnetic button effect works&lt;/h2&gt;
  &lt;p&gt;On each &lt;code&gt;mousemove&lt;/code&gt; event, compute the vector from the cursor to the button center: &lt;code&gt;dx = pointer.x - center.x&lt;/code&gt;, &lt;code&gt;dy = pointer.y - center.y&lt;/code&gt;. Calculate distance with &lt;code&gt;Math.hypot(dx, dy)&lt;/code&gt;. If the cursor is within the activation radius, compute a &lt;code&gt;pull&lt;/code&gt; factor that peaks at 1.0 when the cursor is on-center and fades to 0 at the radius edge. Multiply &lt;code&gt;dx * pull * strength&lt;/code&gt; to get the target offset for the button.&lt;/p&gt;
  &lt;p&gt;Apply linear interpolation (lerp) every animation frame: &lt;code&gt;currentX += (targetX - currentX) * 0.15&lt;/code&gt;. This creates the inertia—the button chases but never instantly reaches its target, which makes the motion feel organic. When the cursor leaves the proximity zone, the target snaps back to (0, 0) and the button eases to rest.&lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;
    &lt;div class=&quot;jh-exp-code-head&quot;&gt;&lt;span&gt;demo.js&lt;/span&gt;&lt;span&gt;KEY IDEA&lt;/span&gt;&lt;/div&gt;
    &lt;pre&gt;&lt;code&gt;const dx = pointer.x - center.x;
const dy = pointer.y - center.y;
const distance = Math.hypot(dx, dy);

const pull = Math.max(0, 1 - distance / radius);
buttonX = center.x + dx * pull * 0.35;
buttonY = center.y + dy * pull * 0.35;&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
  &lt;p class=&quot;jh-exp-caption&quot;&gt;The &lt;code&gt;pull&lt;/code&gt; factor is the whole trick: 1.0 at center, 0.0 at the radius edge. Multiply it by a strength below 1.0 so the button never fully reaches the cursor—that gap is what makes it feel magnetic rather than sticky.&lt;/p&gt;

  &lt;h2&gt;Why this interaction feels great in game menus&lt;/h2&gt;
  &lt;p&gt;The &quot;game feel&quot; in a menu comes from physics-based response. A button that snaps instantly feels mechanical and cheap. A button that follows the cursor with a slight lag but springs back to center feels like it has mass and personality. The lerp factor (0.08–0.2) controls the spring: slow lerp = heavy button, fast lerp = light, snappy button. For a heavy warrior class button, use 0.06. For a fast rogue class button, use 0.2. Same formula, different personality.&lt;/p&gt;
  &lt;p&gt;Agencies like STRV, Locomotive, and Resn use this pattern on call-to-action buttons because it rewards cursor exploration. In a game, it rewards players for exploring the menu. The code is 40 lines including the lerp and the radius guard — drop it into your game&#39;s title screen tonight.&lt;/p&gt;

  &lt;div class=&quot;jh-article-current&quot;&gt;
    &lt;strong&gt;Performance note&lt;/strong&gt;
    &lt;p&gt;Apply &lt;code&gt;will-change: transform&lt;/code&gt; to the magnetic element so the browser pre-promotes it to its own compositing layer. Skip the lerp and apply the transform synchronously inside the &lt;code&gt;mousemove&lt;/code&gt; handler if you want zero-lag snap instead of eased follow. Cancel the animation frame loop when the cursor leaves the proximity zone to avoid unnecessary work.&lt;/p&gt;
  &lt;/div&gt;

  &lt;h2&gt;Ways to extend this pattern&lt;/h2&gt;
  &lt;p&gt;Apply the same proximity-and-lerp formula to multiple elements simultaneously—a group of nav links that each deflect slightly as the cursor passes through creates an immersive navigation experience. Add a subtle &lt;code&gt;rotateX/rotateY&lt;/code&gt; tilt based on the cursor offset relative to the button center for a 3D card-tilt variant. Or drive the magnetic pull strength from a scroll progress value so the button gets stronger as the user scrolls toward it.&lt;/p&gt;

  &lt;h2&gt;Frequently asked questions&lt;/h2&gt;
  &lt;h3&gt;Does the magnetic button effect work on touchscreens?&lt;/h3&gt;
  &lt;p&gt;Yes, using &lt;code&gt;touchmove&lt;/code&gt;. Set &lt;code&gt;touch-action: none&lt;/code&gt; on the element to prevent scroll interference, and read position from &lt;code&gt;event.touches[0].clientX/Y&lt;/code&gt;. The lerp factor makes it feel natural on touch since the element follows the finger with a slight delay rather than teleporting.&lt;/p&gt;
  &lt;h3&gt;How do I tune the feel of the magnetic effect?&lt;/h3&gt;
  &lt;p&gt;Three numbers control everything: &lt;code&gt;radius&lt;/code&gt; (how close the cursor must be to activate), &lt;code&gt;strength&lt;/code&gt; (how far the button travels toward the cursor, typically 0.25–0.45), and the lerp factor (0.08 for sluggish, 0.25 for snappy). Start with strength 0.35 and a radius equal to the button&#39;s diagonal length.&lt;/p&gt;

  &lt;h2&gt;Related experiments&lt;/h2&gt;
  &lt;ul&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-particle-cursor-effect-canvas.html&quot;&gt;JavaScript Particle Cursor Effect with Canvas&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/threejs-particle-cursor-effect.html&quot;&gt;Three.js Particle Cursor Effect&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-neon-mouse-trail.html&quot;&gt;JavaScript Neon Mouse Trail Animation&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;

  &lt;div class=&quot;jh-article-end&quot;&gt;
    &lt;p&gt;Part of a weekly series on visual JavaScript, modern CSS, browser graphics, and HTML5 games—one live experiment per post, production-ready code snippets, and the reasoning behind each technique.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;</description><link>http://juegosenhtml5.blogspot.com/2026/09/javascript-magnetic-button-effect.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-9024404898950860830</guid><pubDate>Tue, 01 Sep 2026 12:06:11 +0000</pubDate><atom:updated>2026-09-02T06:53:12.017-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">browser platformer tutorial&#xa;Labels: lang-en</category><category domain="http://www.blogger.com/atom/ns#">canvas</category><category domain="http://www.blogger.com/atom/ns#">Canvas 2D game physics</category><category domain="http://www.blogger.com/atom/ns#">en-games</category><category domain="http://www.blogger.com/atom/ns#">Games</category><category domain="http://www.blogger.com/atom/ns#">HTML5 platformer game</category><category domain="http://www.blogger.com/atom/ns#">JavaScript</category><category domain="http://www.blogger.com/atom/ns#">JavaScript 2D platformer physics</category><category domain="http://www.blogger.com/atom/ns#">JavaScript gravity game</category><category domain="http://www.blogger.com/atom/ns#">platformer jump JavaScript</category><title>JavaScript 2D Platformer Physics — Jump Arc, Gravity &amp; Coyote Time</title><description>&lt;!--
BLOGGER POST — EN
Title / H1: JavaScript 2D Platformer Physics — Jump Arc, Gravity &amp; Coyote Time
Suggested slug: javascript-2d-platformer-physics
Search description: Learn how to build 2D platformer physics in JavaScript with Canvas — gravity, variable jump height, coyote time, and solid platform collision.
Primary keyword: JavaScript 2D platformer physics
Secondary keywords: HTML5 platformer game, JavaScript gravity game, Canvas 2D game physics, platformer jump JavaScript, browser platformer tutorial
Labels: lang-en, en-games, Games, JavaScript, Canvas, platformer, HTML5 Game
Series: HTML5 Games from Scratch
Social title: 2D Platformer Physics in JavaScript — Jump, Gravity &amp; Coyote Time
Thumbnail text: JUMP!
--&gt;

&lt;article class=&quot;jh-exp&quot;
  data-project=&quot;javascript-2d-platformer-physics&quot;
  data-primary-keyword=&quot;JavaScript 2D platformer physics&quot;&gt;

  &lt;p class=&quot;jh-article-lead&quot;&gt;
    &lt;strong&gt;The jump is the heart of every platformer.&lt;/strong&gt;
    This demo shows a complete 2D platformer physics loop in vanilla JavaScript and Canvas —
    gravity, variable jump height, coyote time so platform edges feel forgiving,
    horizontal acceleration, friction, and solid AABB collision.
    The character moves automatically when idle, but keyboard input instantly takes control.
  &lt;/p&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;

```
&lt;div&gt;
  &lt;strong&gt;What you will build&lt;/strong&gt;

  &lt;p&gt;
    A Canvas character that runs across platforms, jumps with a variable-height arc
    (hold longer = jump higher), and gets 80ms of coyote time after leaving a platform.
    Collision is resolved separately on the X and Y axes for predictable floor,
    wall, and ceiling detection.
  &lt;/p&gt;
&lt;/div&gt;
```

  &lt;/div&gt;

  &lt;!-- =========================================================
       LIVE DEMO
  ========================================================== --&gt;

  &lt;section
    class=&quot;jh-exp-stage&quot;
    id=&quot;demo-javascript_2d_platformer_physics&quot;&gt;

```
&lt;div class=&quot;jh-exp-stage-head&quot;&gt;

  &lt;div&gt;
    &lt;span class=&quot;jh-exp-kicker&quot;&gt;
      LIVE GAME DEMO
    &lt;/span&gt;

    &lt;h2&gt;
      2D Platformer Physics — JavaScript Canvas
    &lt;/h2&gt;
  &lt;/div&gt;

  &lt;span class=&quot;jh-exp-hint&quot;&gt;
    Arrow keys or A/D to move, Space / W / ↑ to jump
  &lt;/span&gt;

&lt;/div&gt;

&lt;div class=&quot;jh-exp-canvas-wrap&quot;&gt;

  &lt;canvas
    class=&quot;jh-exp-canvas&quot;
    aria-label=&quot;JavaScript 2D platformer physics demo&quot;&gt;
  &lt;/canvas&gt;

&lt;/div&gt;
```

  &lt;/section&gt;

  &lt;script&gt;
  (function () {

    const root =
      document.getElementById(
        &#39;demo-javascript_2d_platformer_physics&#39;
      );

    if (!root) return;

    const canvas =
      root.querySelector(&#39;canvas&#39;);

    const ctx =
      canvas.getContext(&#39;2d&#39;);


    /* =====================================================
       CANVAS
    ====================================================== */

    function resize() {

      const rect =
        canvas.getBoundingClientRect();

      const dpr =
        Math.min(
          window.devicePixelRatio || 1,
          2
        );

      canvas.width =
        Math.floor(
          rect.width * dpr
        );

      canvas.height =
        Math.floor(
          rect.height * dpr
        );

      ctx.setTransform(
        dpr,
        0,
        0,
        dpr,
        0,
        0
      );

    }


    resize();


    if (&#39;ResizeObserver&#39; in window) {

      new ResizeObserver(
        resize
      ).observe(canvas);

    } else {

      window.addEventListener(
        &#39;resize&#39;,
        resize
      );

    }


    const W = () =&gt;
      canvas.clientWidth;

    const H = () =&gt;
      canvas.clientHeight;



    /* =====================================================
       WORLD
    ====================================================== */

    const platforms = [

      {
        x: 0,
        y: 280,
        w: 200,
        h: 20
      },

      {
        x: 240,
        y: 240,
        w: 140,
        h: 20
      },

      {
        x: 420,
        y: 200,
        w: 120,
        h: 20
      },

      {
        x: 580,
        y: 250,
        w: 160,
        h: 20
      },

      {
        x: 700,
        y: 290,
        w: 200,
        h: 20
      },

      {
        x: 0,
        y: 310,
        w: 900,
        h: 20
      }

    ];



    /* =====================================================
       PLAYER
    ====================================================== */

    const player = {

      x: 60,
      y: 240,

      w: 22,
      h: 30,

      vx: 0,
      vy: 0,

      onGround: false,

      coyote: 0,

      jumpHeld: false

    };



    /* =====================================================
       PHYSICS
    ====================================================== */

    const GRAVITY = 0.55;

    const JUMP_FORCE = -11;

    const SPEED = 3.5;

    const FRICTION = 0.82;

    const COYOTE_TIME = 80;

    const JUMP_CUT = 0.48;



    /* =====================================================
       INPUT
    ====================================================== */

    const keys = {};


    /*
      When the player uses the keyboard,
      auto mode is temporarily disabled.

      This prevents the automatic demo
      from fighting against manual input.
    */

    let manualUntil = 0;


    const GAME_KEYS =
      new Set([

        &#39;ArrowLeft&#39;,
        &#39;ArrowRight&#39;,
        &#39;ArrowUp&#39;,
        &#39;ArrowDown&#39;,

        &#39;Space&#39;,

        &#39;KeyA&#39;,
        &#39;KeyD&#39;,
        &#39;KeyW&#39;

      ]);


    document.addEventListener(
      &#39;keydown&#39;,
      function (e) {

        if (!GAME_KEYS.has(e.code)) {
          return;
        }

        keys[e.code] = true;

        e.preventDefault();

        manualUntil =
          performance.now() + 1500;

      },
      {
        passive: false
      }
    );


    document.addEventListener(
      &#39;keyup&#39;,
      function (e) {

        if (!GAME_KEYS.has(e.code)) {
          return;
        }

        keys[e.code] = false;

        e.preventDefault();

        manualUntil =
          performance.now() + 1500;

      },
      {
        passive: false
      }
    );


    /*
      Clear keyboard state if the user
      changes browser tabs.
    */

    window.addEventListener(
      &#39;blur&#39;,
      function () {

        for (const key in keys) {
          keys[key] = false;
        }

      }
    );



    /* =====================================================
       AABB COLLISION TEST
    ====================================================== */

    function aabb(a, b) {

      return (

        a.x &lt; b.x + b.w &amp;&amp;

        a.x + a.w &gt; b.x &amp;&amp;

        a.y &lt; b.y + b.h &amp;&amp;

        a.y + a.h &gt; b.y

      );

    }



    /* =====================================================
       RESET
    ====================================================== */

    function resetPlayer() {

      player.x = 60;
      player.y = 220;

      player.vx = 0;
      player.vy = 0;

      player.onGround = false;

      player.coyote = 0;

      player.jumpHeld = false;

    }



    /* =====================================================
       AUTO DEMO
    ====================================================== */

    let autoTime = 0;


    function getAutoInput() {

      /*
        The direction changes slowly so
        the character keeps exploring
        the platforms instead of remaining
        stuck against one edge.
      */

      const moveWave =
        Math.sin(
          autoTime * 0.45
        );


      let left = false;

      let right = false;


      if (moveWave &gt; 0.12) {

        right = true;

      } else if (moveWave &lt; -0.12) {

        left = true;

      }


      /*
        Short automatic jump pulses.
      */

      const jump =
        Math.sin(
          autoTime * 1.35
        ) &gt; 0.91;


      return {

        left: left,

        right: right,

        jump: jump

      };

    }



    /* =====================================================
       GAME LOOP
    ====================================================== */

    let lastTime =
      performance.now();


    function frame(now) {

      const width = W();

      const height = H();


      /*
        Cap delta time so physics does not
        explode after changing browser tabs.
      */

      const delta =
        Math.min(
          32,
          Math.max(
            0,
            now - lastTime
          )
        );


      lastTime = now;


      /*
        Normalized approximately to 60 FPS.
      */

      const dt =
        delta / 16.6667;


      autoTime +=
        delta / 1000;



      /* ===================================================
         MANUAL INPUT
      ==================================================== */

      const manualLeft =

        !!keys[&#39;ArrowLeft&#39;] ||

        !!keys[&#39;KeyA&#39;];


      const manualRight =

        !!keys[&#39;ArrowRight&#39;] ||

        !!keys[&#39;KeyD&#39;];


      const manualJump =

        !!keys[&#39;Space&#39;] ||

        !!keys[&#39;ArrowUp&#39;] ||

        !!keys[&#39;KeyW&#39;];



      /*
        If the user interacted recently,
        manual controls get complete priority.
      */

      const manualMode =
        now &lt; manualUntil;



      /* ===================================================
         AUTO INPUT
      ==================================================== */

      const auto =
        getAutoInput();


      const left =
        manualMode
          ? manualLeft
          : auto.left;


      const right =
        manualMode
          ? manualRight
          : auto.right;


      const jump =
        manualMode
          ? manualJump
          : auto.jump;



      /* ===================================================
         HORIZONTAL MOVEMENT
      ==================================================== */

      if (right &amp;&amp; !left) {

        player.vx +=

          SPEED *

          (1 - FRICTION) *

          dt;

      }


      else if (left &amp;&amp; !right) {

        player.vx -=

          SPEED *

          (1 - FRICTION) *

          dt;

      }


      player.vx *=
        Math.pow(
          FRICTION,
          dt
        );



      /* ===================================================
         COYOTE TIME
      ==================================================== */

      if (player.onGround) {

        player.coyote =
          COYOTE_TIME;

      }

      else {

        player.coyote =
          Math.max(

            0,

            player.coyote - delta

          );

      }



      /* ===================================================
         JUMP INPUT
      ==================================================== */

      const jumpPressed =

        jump &amp;&amp;

        !player.jumpHeld;


      const jumpReleased =

        !jump &amp;&amp;

        player.jumpHeld;



      /*
        Jump is allowed while standing on
        a platform or during the short
        coyote-time window.
      */

      if (

        jumpPressed &amp;&amp;

        player.coyote &gt; 0

      ) {

        player.vy =
          JUMP_FORCE;

        player.coyote = 0;

        player.onGround = false;

      }



      /*
        VARIABLE JUMP HEIGHT

        Releasing jump while the character
        is still moving upward cuts the
        upward velocity.

        Tap = short jump
        Hold = higher jump
      */

      if (

        jumpReleased &amp;&amp;

        player.vy &lt; 0

      ) {

        player.vy *=
          JUMP_CUT;

      }


      player.jumpHeld =
        jump;



      /* ===================================================
         GRAVITY
      ==================================================== */

      player.vy +=
        GRAVITY * dt;



      /* ===================================================
         X MOVEMENT
      ==================================================== */

      player.x +=
        player.vx * dt;



      /* ===================================================
         X COLLISION
      ==================================================== */

      for (
        const platform of platforms
      ) {

        if (
          !aabb(
            player,
            platform
          )
        ) {
          continue;
        }


        const overlapX =

          Math.min(

            player.x + player.w,

            platform.x + platform.w

          )

          -

          Math.max(

            player.x,

            platform.x

          );


        const overlapY =

          Math.min(

            player.y + player.h,

            platform.y + platform.h

          )

          -

          Math.max(

            player.y,

            platform.y

          );


        if (
          overlapX &lt; overlapY
        ) {

          const playerCenter =

            player.x +

            player.w / 2;


          const platformCenter =

            platform.x +

            platform.w / 2;


          if (
            playerCenter &lt;
            platformCenter
          ) {

            player.x -=
              overlapX;

          }

          else {

            player.x +=
              overlapX;

          }


          player.vx = 0;

        }

      }



      /* ===================================================
         Y MOVEMENT
      ==================================================== */

      player.y +=
        player.vy * dt;


      player.onGround =
        false;



      /* ===================================================
         Y COLLISION
      ==================================================== */

      for (
        const platform of platforms
      ) {

        if (
          !aabb(
            player,
            platform
          )
        ) {
          continue;
        }


        const overlapX =

          Math.min(

            player.x + player.w,

            platform.x + platform.w

          )

          -

          Math.max(

            player.x,

            platform.x

          );


        const overlapY =

          Math.min(

            player.y + player.h,

            platform.y + platform.h

          )

          -

          Math.max(

            player.y,

            platform.y

          );


        if (
          overlapY &lt;= overlapX
        ) {

          const playerCenter =

            player.y +

            player.h / 2;


          const platformCenter =

            platform.y +

            platform.h / 2;



          /*
            Landing on top.
          */

          if (
            playerCenter &lt;
            platformCenter
          ) {

            player.y =

              platform.y -

              player.h;


            player.vy = 0;

            player.onGround =
              true;

          }


          /*
            Hitting a platform from below.
          */

          else {

            player.y =

              platform.y +

              platform.h;


            player.vy = 0;

          }

        }

      }



      /* ===================================================
         WORLD BOUNDS
      ==================================================== */

      if (
        player.x &lt; 0
      ) {

        player.x = 0;

        player.vx = 0;

      }


      if (
        player.x + player.w &gt;
        width
      ) {

        player.x =
          width - player.w;

        player.vx = 0;

      }


      /*
        Respawn if the player falls
        below the canvas.
      */

      if (
        player.y &gt;
        height + 80
      ) {

        resetPlayer();

      }



      /* ===================================================
         DRAW BACKGROUND
      ==================================================== */

      ctx.clearRect(
        0,
        0,
        width,
        height
      );


      ctx.fillStyle =
        &#39;#040d14&#39;;


      ctx.fillRect(
        0,
        0,
        width,
        height
      );



      /* ===================================================
         GRID
      ==================================================== */

      ctx.strokeStyle =
        &#39;rgba(8,100,120,0.18)&#39;;


      ctx.lineWidth = 1;


      for (
        let x = 0;
        x &lt; width;
        x += 40
      ) {

        ctx.beginPath();

        ctx.moveTo(
          x,
          0
        );

        ctx.lineTo(
          x,
          height
        );

        ctx.stroke();

      }


      for (
        let y = 0;
        y &lt; height;
        y += 40
      ) {

        ctx.beginPath();

        ctx.moveTo(
          0,
          y
        );

        ctx.lineTo(
          width,
          y
        );

        ctx.stroke();

      }



      /* ===================================================
         PLATFORMS
      ==================================================== */

      for (
        const platform of platforms
      ) {

        ctx.fillStyle =
          &#39;#0d3a4e&#39;;


        ctx.fillRect(

          platform.x,

          platform.y,

          platform.w,

          platform.h

        );


        ctx.fillStyle =
          &#39;#08c8e4&#39;;


        ctx.fillRect(

          platform.x,

          platform.y,

          platform.w,

          4

        );

      }



      /* ===================================================
         PLAYER SHADOW
      ==================================================== */

      ctx.fillStyle =
        &#39;rgba(0,0,0,0.25)&#39;;


      ctx.fillRect(

        player.x + 3,

        player.y + player.h + 3,

        player.w,

        4

      );



      /* ===================================================
         PLAYER
      ==================================================== */

      ctx.fillStyle =
        &#39;#ff7b1a&#39;;


      ctx.fillRect(

        player.x,

        player.y,

        player.w,

        player.h

      );


      /*
        Eye
      */

      ctx.fillStyle =
        &#39;#ffffff&#39;;


      ctx.fillRect(

        player.x + 5,

        player.y + 5,

        5,

        5

      );



      /* ===================================================
         VELOCITY ARROW
      ==================================================== */

      if (
        Math.abs(player.vy) &gt; 1
      ) {

        ctx.strokeStyle =

          player.vy &lt; 0

            ? &#39;#20ff80&#39;

            : &#39;rgba(255,100,0,0.75)&#39;;


        ctx.lineWidth = 2;


        const centerX =

          player.x +

          player.w / 2;


        const startY =
          player.y;


        const endY =

          startY -

          player.vy * 3;


        ctx.beginPath();


        ctx.moveTo(

          centerX,

          startY

        );


        ctx.lineTo(

          centerX,

          endY

        );


        ctx.stroke();

      }



      /* ===================================================
         DEBUG LABELS
      ==================================================== */

      ctx.fillStyle =
        &#39;rgba(8,200,228,0.85)&#39;;


      ctx.font =
        &#39;11px monospace&#39;;


      ctx.fillText(

        &#39;GRAVITY: &#39; +
        GRAVITY,

        8,

        16

      );


      ctx.fillText(

        &#39;VY: &#39; +
        player.vy.toFixed(1),

        8,

        30

      );


      ctx.fillText(

        &#39;COYOTE: &#39; +

        (
          player.coyote &gt; 0
            ? &#39;ACTIVE&#39;
            : &#39;--&#39;
        ),

        8,

        44

      );


      ctx.fillText(

        manualMode
          ? &#39;CONTROL: PLAYER&#39;
          : &#39;CONTROL: AUTO&#39;,

        8,

        58

      );


      requestAnimationFrame(
        frame
      );

    }


    requestAnimationFrame(
      frame
    );

  })();
  &lt;/script&gt;

  &lt;!-- =========================================================
       ARTICLE
  ========================================================== --&gt;

  &lt;h2&gt;
    How 2D platformer physics works in JavaScript
  &lt;/h2&gt;

  &lt;p&gt;
    A simple platformer does not need a full physics engine.
    Each frame, the game updates horizontal velocity, applies gravity to vertical velocity,
    moves the character, checks for collisions, and corrects any overlap with the platforms.
  &lt;/p&gt;

  &lt;p&gt;
    The important part is resolving horizontal and vertical movement separately.
    First the player moves on the X axis and wall collisions are resolved.
    Then the player moves on the Y axis and floor or ceiling collisions are resolved.
    This approach keeps collision behavior predictable and avoids many common
    corner-sticking problems found in beginner platformer code.
  &lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;

```
&lt;div class=&quot;jh-exp-code-head&quot;&gt;
  &lt;span&gt;platformer.js&lt;/span&gt;
  &lt;span&gt;KEY IDEA&lt;/span&gt;
&lt;/div&gt;

&lt;pre&gt;&lt;code data-code-source&gt;// Gravity
```

player.vy += GRAVITY * dt;

// Horizontal movement
player.x += player.vx * dt;
resolveCollisionsX(player, platforms);

// Vertical movement
player.y += player.vy * dt;
resolveCollisionsY(player, platforms);

// Jump
if (jumpPressed &amp;&amp; player.coyote &gt; 0) {
player.vy = JUMP_FORCE;
player.coyote = 0;
}&lt;/code&gt;&lt;/pre&gt;

  &lt;/div&gt;

  &lt;p class=&quot;jh-exp-caption&quot;&gt;
    Separating horizontal and vertical collision resolution makes floor,
    wall, and ceiling detection easier to control and debug.
  &lt;/p&gt;

  &lt;h2&gt;
    Variable jump height — tap or hold
  &lt;/h2&gt;

  &lt;p&gt;
    Good platformer jumps are usually not fixed.
    A quick tap should create a short jump, while holding the jump button
    should allow the character to travel higher.
  &lt;/p&gt;

  &lt;p&gt;
    This demo implements variable jump height by watching for the moment
    the jump button is released. If the player is still moving upward,
    the game immediately reduces the upward velocity.
  &lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;

```
&lt;div class=&quot;jh-exp-code-head&quot;&gt;
  &lt;span&gt;jump-cut.js&lt;/span&gt;
  &lt;span&gt;VARIABLE HEIGHT&lt;/span&gt;
&lt;/div&gt;

&lt;pre&gt;&lt;code data-code-source&gt;const jumpReleased =
```

!jump &amp;&amp; player.jumpHeld;

if (jumpReleased &amp;&amp; player.vy &lt; 0) {
player.vy *= JUMP_CUT;
}&lt;/code&gt;&lt;/pre&gt;

  &lt;/div&gt;

  &lt;p&gt;
    The result is simple: tap the button for a low jump or hold it for a higher arc.
    This small change gives the player much more control over landing precision.
  &lt;/p&gt;

  &lt;h2&gt;
    Coyote time — why platformers feel more forgiving
  &lt;/h2&gt;

  &lt;p&gt;
    Coyote time gives the player a tiny window to jump after walking off
    the edge of a platform.
    Without it, pressing jump one frame too late can feel unresponsive.
    With coyote time, the controls tolerate a small amount of human timing error.
  &lt;/p&gt;

  &lt;p&gt;
    This demo uses an 80ms coyote-time window.
    Whenever the player is standing on a platform, the timer resets.
    After leaving the platform, the timer counts down toward zero.
    A jump is still allowed while that timer remains active.
  &lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;

```
&lt;div class=&quot;jh-exp-code-head&quot;&gt;
  &lt;span&gt;coyote-time.js&lt;/span&gt;
  &lt;span&gt;80 MS WINDOW&lt;/span&gt;
&lt;/div&gt;

&lt;pre&gt;&lt;code data-code-source&gt;if (player.onGround) {
```

player.coyote = COYOTE_TIME;
} else {
player.coyote =
Math.max(0, player.coyote - delta);
}

if (jumpPressed &amp;&amp; player.coyote &gt; 0) {
player.vy = JUMP_FORCE;
player.coyote = 0;
}&lt;/code&gt;&lt;/pre&gt;

  &lt;/div&gt;

  &lt;h2&gt;
    Automatic demo without fighting the player
  &lt;/h2&gt;

  &lt;p&gt;
    Interactive examples embedded inside articles have one useful problem:
    if the character starts completely still, some readers may never realize
    the Canvas is playable.
  &lt;/p&gt;

  &lt;p&gt;
    This demo therefore moves automatically while idle.
    However, automatic input must never compete with real keyboard input.
    The moment the reader presses a movement or jump key,
    automatic movement is disabled and manual controls receive full priority.
  &lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;

```
&lt;div class=&quot;jh-exp-code-head&quot;&gt;
  &lt;span&gt;input.js&lt;/span&gt;
  &lt;span&gt;PLAYER PRIORITY&lt;/span&gt;
&lt;/div&gt;

&lt;pre&gt;&lt;code data-code-source&gt;const manualMode =
```

now &lt; manualUntil;

const left =
manualMode ? manualLeft : auto.left;

const right =
manualMode ? manualRight : auto.right;

const jump =
manualMode ? manualJump : auto.jump;&lt;/code&gt;&lt;/pre&gt;

  &lt;/div&gt;

  &lt;p&gt;
    After a short period without keyboard input, the automatic demo resumes.
    This keeps the example visually active while still making the controls
    feel immediate when the reader interacts with it.
  &lt;/p&gt;

  &lt;h2&gt;
    Why the original input logic could fail
  &lt;/h2&gt;

  &lt;p&gt;
    A common mistake is combining automatic and manual movement in the same condition:
  &lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;

```
&lt;div class=&quot;jh-exp-code-head&quot;&gt;
  &lt;span&gt;wrong.js&lt;/span&gt;
  &lt;span&gt;INPUT BUG&lt;/span&gt;
&lt;/div&gt;

&lt;pre&gt;&lt;code data-code-source&gt;if (rightKey || autoRight) {
```

player.vx += acceleration;
} else if (leftKey) {
player.vx -= acceleration;
}&lt;/code&gt;&lt;/pre&gt;

  &lt;/div&gt;

  &lt;p&gt;
    If &lt;code&gt;autoRight&lt;/code&gt; is true, the first condition wins even when
    the player is pressing left. The automatic demo effectively fights the keyboard.
  &lt;/p&gt;

  &lt;p&gt;
    The corrected version keeps automatic and manual input separate
    and explicitly selects which input source controls the character.
  &lt;/p&gt;

  &lt;h2&gt;
    Axis-separated AABB collision
  &lt;/h2&gt;

  &lt;p&gt;
    The platforms and player are rectangles, so collision detection uses
    axis-aligned bounding boxes, usually called AABB collision.
  &lt;/p&gt;

  &lt;p&gt;
    An overlap exists when the player&#39;s rectangle intersects a platform
    on both the horizontal and vertical axes.
    Once an overlap is found, the game compares the penetration depth
    and pushes the character out along the appropriate axis.
  &lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;

```
&lt;div class=&quot;jh-exp-code-head&quot;&gt;
  &lt;span&gt;aabb.js&lt;/span&gt;
  &lt;span&gt;COLLISION&lt;/span&gt;
&lt;/div&gt;

&lt;pre&gt;&lt;code data-code-source&gt;function aabb(a, b) {
```

return (
a.x &lt; b.x + b.w &amp;&amp;
a.x + a.w &gt; b.x &amp;&amp;
a.y &lt; b.y + b.h &amp;&amp;
a.y + a.h &gt; b.y
);
}&lt;/code&gt;&lt;/pre&gt;

  &lt;/div&gt;

  &lt;h2&gt;
    Delta time keeps movement more consistent
  &lt;/h2&gt;

  &lt;p&gt;
    Browser animation is usually close to 60 frames per second,
    but it is not guaranteed to run at exactly the same speed on every frame.
    That is why this version calculates a small delta-time multiplier.
  &lt;/p&gt;

  &lt;p&gt;
    Movement and gravity are multiplied by that value,
    which reduces visible speed differences when frames are slightly delayed.
    The delta value is also capped so switching browser tabs does not cause
    a huge physics update when the tab becomes active again.
  &lt;/p&gt;

  &lt;div class=&quot;jh-article-current&quot;&gt;

```
&lt;strong&gt;
  Add this physics loop to your HTML5 game
&lt;/strong&gt;

&lt;p&gt;
  This structure can be used as the foundation of a larger Canvas platformer.
  Replace the rectangle with a sprite, add animation states,
  move platforms into level data, and introduce a camera for scrolling worlds.
  The same player state can also be extended with wall detection,
  dash mechanics, one-way platforms, enemies, checkpoints, and collectibles.
&lt;/p&gt;
```

  &lt;/div&gt;

  &lt;h2&gt;
    Next steps: make it a real game
  &lt;/h2&gt;

  &lt;p&gt;
    Once the base movement feels good, add features one at a time.
    One-way platforms can ignore upward collisions and only catch the player while falling.
    Wall jumps can detect horizontal contact and apply a vertical impulse.
    Moving platforms can update their own position before player collision is resolved.
  &lt;/p&gt;

  &lt;p&gt;
    For a side-scrolling level, add a camera offset instead of moving the entire world manually.
    Draw platforms and the player relative to the camera position,
    while keeping physics coordinates in world space.
  &lt;/p&gt;

  &lt;p&gt;
    From there, the most important improvement is not adding more physics —
    it is tuning the values you already have.
    Experiment with &lt;code&gt;GRAVITY&lt;/code&gt;, &lt;code&gt;JUMP_FORCE&lt;/code&gt;,
    &lt;code&gt;FRICTION&lt;/code&gt;, &lt;code&gt;JUMP_CUT&lt;/code&gt;, and
    &lt;code&gt;COYOTE_TIME&lt;/code&gt; until movement feels responsive.
    In platformers, small timing changes can completely change how the game feels.
  &lt;/p&gt;

&lt;/article&gt;
</description><link>http://juegosenhtml5.blogspot.com/2026/09/javascript-2d-platformer-physics.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-3962910689237398962</guid><pubDate>Tue, 01 Sep 2026 11:49:31 +0000</pubDate><atom:updated>2026-09-02T06:49:39.419-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">arcade game</category><category domain="http://www.blogger.com/atom/ns#">browser game JavaScript</category><category domain="http://www.blogger.com/atom/ns#">en-games</category><category domain="http://www.blogger.com/atom/ns#">Games</category><category domain="http://www.blogger.com/atom/ns#">HTML5 Breakout game</category><category domain="http://www.blogger.com/atom/ns#">HTML5 Game</category><category domain="http://www.blogger.com/atom/ns#">HTML5 game tutorial&#xa;Labels: lang-en</category><category domain="http://www.blogger.com/atom/ns#">lang-en</category><category domain="http://www.blogger.com/atom/ns#">phaser</category><category domain="http://www.blogger.com/atom/ns#">Phaser 4</category><category domain="http://www.blogger.com/atom/ns#">Phaser 4 Breakout game</category><category domain="http://www.blogger.com/atom/ns#">Phaser 4 brick breaker</category><category domain="http://www.blogger.com/atom/ns#">Phaser 4 tutorial</category><title>Build a Phaser 4 Breakout Game — Complete HTML5 Browser Tutorial</title><description>&lt;!--
BLOGGER POST — EN
Title / H1: Build a Phaser 4 Breakout Game — Complete HTML5 Browser Tutorial
Suggested slug: phaser-4-breakout-game
Search description: Build a complete Breakout brick-breaker game in Phaser 4 with ball physics, paddle, destructible bricks, score, and lives — playable in the browser, under 80 lines.
Primary keyword: Phaser 4 Breakout game
Secondary keywords: HTML5 Breakout game, Phaser 4 tutorial, browser game JavaScript, Phaser 4 brick breaker, HTML5 game tutorial
Labels: lang-en, en-games, Games, Phaser, HTML5 Game, Phaser 4, arcade game
Series: HTML5 Games from Scratch
Social title: I Built Breakout in Phaser 4 in 80 Lines
Thumbnail text: PLAY NOW
--&gt;

&lt;article class=&quot;jh-exp&quot; data-project=&quot;phaser-4-breakout-game&quot; data-primary-keyword=&quot;Phaser 4 Breakout game&quot;&gt;

  &lt;p class=&quot;jh-article-lead&quot;&gt;
    &lt;strong&gt;Build a fully playable Breakout brick-breaker in Phaser 4&lt;/strong&gt; —
    paddle, ball, 40 destructible bricks, score counter, and three lives —
    entirely in the browser, no downloads, no assets. Under 80 lines of Phaser 4 Scene code.
    The game auto-plays an AI paddle so you can watch the physics in action,
    or grab the mouse to play yourself.
  &lt;/p&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;
    &lt;div&gt;
      &lt;strong&gt;What you will build&lt;/strong&gt;
      &lt;p&gt;
        A Phaser 4 scene with a ball bouncing off walls, a paddle controlled by the mouse
        (with AI fallback for auto-play), 40 bricks arranged in 5 color-coded rows,
        a score counter, and a lives system. When all bricks are cleared the game resets —
        a clean loop you can fork as an arcade game starter.
      &lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;section class=&quot;jh-exp-stage&quot; id=&quot;demo-phaser_4_breakout_game&quot;&gt;
    &lt;div class=&quot;jh-exp-stage-head&quot;&gt;
      &lt;div&gt;
        &lt;span class=&quot;jh-exp-kicker&quot;&gt;PHASER 4 LIVE GAME&lt;/span&gt;
        &lt;h2&gt;Phaser 4 Breakout — Play in Browser&lt;/h2&gt;
      &lt;/div&gt;
      &lt;span class=&quot;jh-exp-hint&quot;&gt;
        Move the mouse to control the paddle — or watch the AI play
      &lt;/span&gt;
    &lt;/div&gt;

    &lt;div class=&quot;jh-exp-canvas-wrap&quot; data-phaser-host=&quot;1&quot;&gt;&lt;/div&gt;
  &lt;/section&gt;

  &lt;script src=&quot;https://cdn.jsdelivr.net/npm/phaser@4.2.1/dist/phaser.min.js&quot;&gt;&lt;/script&gt;

  &lt;script&gt;
  (function () {
    const root = document.getElementById(&#39;demo-phaser_4_breakout_game&#39;);
    const host = root.querySelector(&#39;[data-phaser-host]&#39;);

    class Breakout extends Phaser.Scene {

      create() {
        this.cameras.main.setBackgroundColor(&#39;#06141d&#39;);

        const C = [
          0xff3a3a,
          0xff8a20,
          0xffd020,
          0x30d880,
          0x20aaff
        ];

        /*
         * IMPORTANT FOR BLOGGER:
         * Do not write the heart character directly in JavaScript.
         * Blogger may convert it to &amp;#9829; inside &lt;script&gt;.
         * Generate it at runtime instead.
         */
        this.heart = String.fromCharCode(9829);

        this.bricks = [];

        for (let r = 0; r &lt; 5; r++) {
          for (let c = 0; c &lt; 8; c++) {
            const b = this.add.rectangle(
              60 + c * 76,
              55 + r * 28,
              68,
              20,
              C[r]
            );

            b.alive = true;
            this.bricks.push(b);
          }
        }

        this.paddle = this.add.rectangle(
          360,
          300,
          100,
          12,
          0x08c8e4
        );

        this.ball = this.add.circle(
          360,
          276,
          7,
          0xffffff
        );

        this.bvx = 3.8;
        this.bvy = -3.8;

        this.score = 0;
        this.lives = 3;

        this.scoreTxt = this.add.text(
          8,
          4,
          &#39;SCORE: 0&#39;,
          {
            color: &#39;#08c8e4&#39;,
            fontSize: &#39;13px&#39;,
            fontFamily: &#39;monospace&#39;
          }
        );

        this.livesTxt = this.add.text(
          620,
          4,
          this.heart.repeat(this.lives),
          {
            color: &#39;#ff4466&#39;,
            fontSize: &#39;13px&#39;,
            fontFamily: &#39;monospace&#39;
          }
        );

        this.autoX = 360;

        this.input.on(&#39;pointermove&#39;, e =&gt; {
          this.autoX = e.x;
        });
      }

      update() {

        // AI paddle follows ball for video capture
        this.autoX += (this.ball.x - this.autoX) * 0.09;

        this.paddle.x = Phaser.Math.Clamp(
          this.autoX,
          52,
          668
        );

        // Move ball
        this.ball.x += this.bvx;
        this.ball.y += this.bvy;

        // Side walls
        if (this.ball.x &lt; 7 || this.ball.x &gt; 713) {
          this.bvx *= -1;
        }

        // Top wall
        if (this.ball.y &lt; 7) {
          this.bvy *= -1;
        }

        // Paddle bounce with angle
        if (
          this.ball.y &gt; this.paddle.y - 14 &amp;&amp;
          this.ball.y &lt; this.paddle.y + 6 &amp;&amp;
          Math.abs(this.ball.x - this.paddle.x) &lt; 56 &amp;&amp;
          this.bvy &gt; 0
        ) {
          const off = (this.ball.x - this.paddle.x) / 56;

          this.bvx = off * 5.5;
          this.bvy = -Math.abs(this.bvy);
        }

        // Brick collision
        for (const b of this.bricks) {

          if (!b.alive) continue;

          if (
            Math.abs(this.ball.x - b.x) &lt; 38 &amp;&amp;
            Math.abs(this.ball.y - b.y) &lt; 17
          ) {
            b.alive = false;
            b.setVisible(false);

            this.bvy *= -1;

            this.score += 10;

            this.scoreTxt.setText(
              &#39;SCORE: &#39; + this.score
            );

            break;
          }
        }

        // Lose ball
        if (this.ball.y &gt; 330) {

          this.lives--;

          this.livesTxt.setText(
            this.heart.repeat(
              Math.max(0, this.lives)
            )
          );

          this.ball.setPosition(
            360,
            240
          );

          this.bvx =
            3.8 * (Math.random() &gt; 0.5 ? 1 : -1);

          this.bvy = -3.8;

          // Game over → reset game
          if (this.lives &lt;= 0) {

            this.lives = 3;
            this.score = 0;

            this.scoreTxt.setText(
              &#39;SCORE: 0&#39;
            );

            this.livesTxt.setText(
              this.heart.repeat(this.lives)
            );

            this.bricks.forEach(b =&gt; {
              b.alive = true;
              b.setVisible(true);
            });
          }
        }

        // Clear all bricks → reset bricks and ball
        if (
          this.bricks.every(
            b =&gt; !b.alive
          )
        ) {

          this.bricks.forEach(b =&gt; {
            b.alive = true;
            b.setVisible(true);
          });

          this.ball.setPosition(
            360,
            240
          );

          this.bvx =
            3.8 * (Math.random() &gt; 0.5 ? 1 : -1);

          this.bvy = -3.8;
        }
      }
    }

    new Phaser.Game({
      type: Phaser.AUTO,
      parent: host,
      width: 720,
      height: 340,
      transparent: true,
      scene: Breakout,

      scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_BOTH
      }
    });

  })();
  &lt;/script&gt;

  &lt;h2&gt;How the Phaser 4 Breakout game is structured&lt;/h2&gt;

  &lt;p&gt;
    The entire game lives in a single &lt;code&gt;Phaser.Scene&lt;/code&gt; class with two methods:
    &lt;code&gt;create()&lt;/code&gt; sets up the scene objects once, and &lt;code&gt;update()&lt;/code&gt;
    runs every frame. Bricks are stored in an array of
    &lt;code&gt;Phaser.GameObjects.Rectangle&lt;/code&gt; objects, each with an
    &lt;code&gt;alive&lt;/code&gt; flag. When a brick is hit, the flag is set to
    &lt;code&gt;false&lt;/code&gt; and the rectangle is hidden — no physics engine required
    for a game this size.
  &lt;/p&gt;

  &lt;p&gt;
    Ball physics is manual: add velocity to position each frame, flip the sign on collision.
    Paddle angle is computed from how far off-center the ball hits:
    &lt;code&gt;const offset = (ball.x - paddle.x) / halfPaddleWidth&lt;/code&gt;.
    Multiplied by a max speed of 5.5, this gives a range from sharp angles at the edges
    to straight bounces at the center — the core feel of Breakout.
  &lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;
    &lt;div class=&quot;jh-exp-code-head&quot;&gt;
      &lt;span&gt;breakout.js&lt;/span&gt;
      &lt;span&gt;KEY IDEA&lt;/span&gt;
    &lt;/div&gt;

    &lt;pre&gt;&lt;code data-code-source&gt;// Place bricks in a grid
for (let row = 0; row &amp;lt; 5; row++) {
  for (let col = 0; col &amp;lt; 8; col++) {
    const brick = this.add.rectangle(
      60 + col * 76, 55 + row * 28,
      68, 20, rowColors[row]
    );
    brick.alive = true;
    this.bricks.push(brick);
  }
}

// Angle the ball based on paddle hit position
const offset = (ball.x - paddle.x) / halfPaddle;
ball.vx = offset * MAX_SPEED;
ball.vy = -Math.abs(ball.vy);&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;

  &lt;p class=&quot;jh-exp-caption&quot;&gt;
    Breakout needs no physics engine — manual velocity + AABB collision is 10 lines
    and runs at 60 fps in every browser.
  &lt;/p&gt;

  &lt;h2&gt;Why Phaser 4 is ideal for HTML5 arcade games&lt;/h2&gt;

  &lt;p&gt;
    Phaser 4&#39;s &lt;code&gt;Scene&lt;/code&gt; API gives you a deterministic
    &lt;code&gt;create → update&lt;/code&gt; loop, an automatic renderer that switches between
    WebGL and Canvas 2D, and built-in input events — everything Breakout needs.
    The &lt;code&gt;Phaser.Scale.FIT&lt;/code&gt; mode handles all screen sizes automatically,
    so your game looks correct on mobile and desktop without a single line of resize code.
  &lt;/p&gt;

  &lt;p&gt;
    For a game like Breakout, you don&#39;t need Phaser&#39;s physics engine
    (Arcade or Matter.js). Manual velocity math is faster to write, easier to debug,
    and runs identically everywhere. Reserve Phaser&#39;s physics for games with complex
    collision shapes or many interacting bodies.
  &lt;/p&gt;

  &lt;div class=&quot;jh-article-current&quot;&gt;
    &lt;strong&gt;Performance note&lt;/strong&gt;

    &lt;p&gt;
      40 bricks as &lt;code&gt;Rectangle&lt;/code&gt; game objects are batched into a single draw call
      in Phaser&#39;s WebGL renderer. If you scale to hundreds of bricks, switch to a
      &lt;code&gt;Phaser.GameObjects.Graphics&lt;/code&gt; or a tilemap for even lower draw-call overhead.
    &lt;/p&gt;
  &lt;/div&gt;

  &lt;h2&gt;Extend this into a full game&lt;/h2&gt;

  &lt;p&gt;
    Add power-ups by spawning a falling rectangle when a brick is destroyed.
    Add multiple balls for chaos mode. Randomize brick layouts each round for a roguelike
    Breakout. Replace the rectangles with sprite textures using
    &lt;code&gt;this.add.image()&lt;/code&gt; for pixel-art bricks. Add a high-score system with
    &lt;code&gt;localStorage&lt;/code&gt; in three lines. This mini game is a complete,
    forkable foundation.
  &lt;/p&gt;

&lt;/article&gt;</description><link>http://juegosenhtml5.blogspot.com/2026/09/phaser-4-breakout-game.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-9173387086963407989</guid><pubDate>Tue, 01 Sep 2026 11:45:04 +0000</pubDate><atom:updated>2026-09-02T06:56:31.022-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">canvas</category><category domain="http://www.blogger.com/atom/ns#">Canvas particle explosion</category><category domain="http://www.blogger.com/atom/ns#">en-games</category><category domain="http://www.blogger.com/atom/ns#">game hit effect JavaScript</category><category domain="http://www.blogger.com/atom/ns#">Games</category><category domain="http://www.blogger.com/atom/ns#">HTML5 game particle effect</category><category domain="http://www.blogger.com/atom/ns#">javascript</category><category domain="http://www.blogger.com/atom/ns#">JavaScript game FX</category><category domain="http://www.blogger.com/atom/ns#">JavaScript particle effect HTML5 game</category><category domain="http://www.blogger.com/atom/ns#">particle system browser game&#xa;lang-en</category><title>JavaScript Particle Hit Effect for HTML5 Games — Canvas Tutorial</title><description>&lt;!--
BLOGGER POST — EN
Title / H1: JavaScript Particle Hit Effect for HTML5 Games — Canvas Tutorial
Suggested slug: javascript-particle-cursor-effect-canvas
Search description: Build a JavaScript particle effect for HTML5 games — explosions, magic spells, and hit FX using Canvas 2D. Under 60 lines, no libraries, runs in every browser.
Primary keyword: JavaScript particle effect HTML5 game
Secondary keywords: HTML5 game particle effect, Canvas particle explosion, JavaScript game FX, game hit effect JavaScript, particle system browser game
Labels: lang-en, en-games, Games, JavaScript, Canvas, particle effect, HTML5 Game
Series: HTML5 Games from Scratch
Social title: Build a Game Particle Effect in JavaScript — No Libraries
Thumbnail text: HIT FX!
--&gt;
&lt;article class=&quot;jh-exp&quot; data-project=&quot;javascript-particle-cursor-effect-canvas&quot; data-primary-keyword=&quot;JavaScript particle effect HTML5 game&quot;&gt;
  &lt;p class=&quot;jh-article-lead&quot;&gt;&lt;strong&gt;Every great game hit, explosion, or magic spell needs particles.&lt;/strong&gt; This demo shows the complete particle system in 60 lines of vanilla JavaScript Canvas — a repulsion field that scatters 520 glowing particles on contact, exactly like an enemy hit reaction or a spell impact in an HTML5 game. Zero libraries, zero build step, 60 fps on every device.&lt;/p&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;
    &lt;div&gt;
      &lt;strong&gt;What you will build&lt;/strong&gt;
      &lt;p&gt;A pointer-reactive particle field that responds to both mouse and touch at 60 fps. Each particle&#39;s velocity is computed from cursor proximity, creating an organic repulsion wave that feels alive on any screen size.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;section class=&quot;jh-exp-stage&quot; id=&quot;demo-javascript_particle_cursor_effect_canvas&quot;&gt;
  &lt;div class=&quot;jh-exp-stage-head&quot;&gt;
    &lt;div&gt;&lt;span class=&quot;jh-exp-kicker&quot;&gt;LIVE EXPERIMENT&lt;/span&gt;&lt;h2&gt;Particle cursor field&lt;/h2&gt;&lt;/div&gt;
    &lt;span class=&quot;jh-exp-hint&quot;&gt;Move the pointer through the particles&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-exp-canvas-wrap&quot;&gt;
    &lt;canvas class=&quot;jh-exp-canvas&quot; aria-label=&quot;Particle cursor field interactive demo&quot;&gt;&lt;/canvas&gt;
  &lt;/div&gt;
  
&lt;/section&gt;
&lt;script&gt;
(function(){
  const root=document.getElementById(&#39;demo-javascript_particle_cursor_effect_canvas&#39;);
  const canvas=root.querySelector(&#39;canvas&#39;);
  const ctx=canvas.getContext(&#39;2d&#39;);
  function resize(){
    const r=canvas.getBoundingClientRect();
    const dpr=Math.min(window.devicePixelRatio||1,2);
    canvas.width=Math.max(2,Math.floor(r.width*dpr));
    canvas.height=Math.max(2,Math.floor(r.height*dpr));
    ctx.setTransform(dpr,0,0,dpr,0,0);
  }
  resize();
  new ResizeObserver(resize).observe(canvas);
  
const particles=[];for(let i=0;i&lt;520;i++)particles.push({x:Math.random()*900,y:Math.random()*460,vx:0,vy:0});
let mouse={x:-999,y:-999};
canvas.addEventListener(&#39;pointermove&#39;,e=&gt;{const r=canvas.getBoundingClientRect();mouse.x=e.clientX-r.left;mouse.y=e.clientY-r.top});
function frame(){const w=canvas.clientWidth,h=canvas.clientHeight;ctx.fillStyle=&#39;#020a10&#39;;ctx.fillRect(0,0,w,h);for(const p of particles){const dx=p.x-mouse.x,dy=p.y-mouse.y,d2=dx*dx+dy*dy;if(d2&lt;9000){const f=(1-Math.sqrt(d2)/95)*1.8;p.vx+=dx/(Math.sqrt(d2)+1)*f;p.vy+=dy/(Math.sqrt(d2)+1)*f}p.vx*=.93;p.vy*=.93;p.x+=p.vx;p.y+=p.vy;if(p.x&lt;0)p.x=w;if(p.x&gt;w)p.x=0;if(p.y&lt;0)p.y=h;if(p.y&gt;h)p.y=0;ctx.fillStyle=&#39;rgba(70,220,255,.82)&#39;;ctx.fillRect(p.x,p.y,1.7,1.7)}requestAnimationFrame(frame)}frame();

})();
&lt;/script&gt;

  &lt;h2&gt;How the JavaScript particle cursor effect works&lt;/h2&gt;
  &lt;p&gt;The effect keeps a flat array of 520 particles, each with just four numbers: &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, &lt;code&gt;vx&lt;/code&gt;, &lt;code&gt;vy&lt;/code&gt;. On every &lt;code&gt;requestAnimationFrame&lt;/code&gt; tick the loop measures squared distance from the cursor to each particle—skipping the expensive &lt;code&gt;Math.sqrt&lt;/code&gt; until the force threshold is confirmed—then applies a velocity impulse inversely proportional to distance. A friction factor of &lt;code&gt;0.93&lt;/code&gt; damps each velocity component per frame, and coordinates wrap at the canvas boundary so particles never escape.&lt;/p&gt;
  &lt;p&gt;The key architectural split is between physics (update velocities and positions) and rendering (clear canvas, draw each particle). Keeping these two passes separate means you can tune them independently: swap the repulsion formula without touching the draw code, or change the particle color scheme without re-testing the physics.&lt;/p&gt;

  &lt;div class=&quot;jh-exp-codebox&quot;&gt;
    &lt;div class=&quot;jh-exp-code-head&quot;&gt;&lt;span&gt;demo.js&lt;/span&gt;&lt;span&gt;KEY IDEA&lt;/span&gt;&lt;/div&gt;
    &lt;pre&gt;&lt;code&gt;canvas.addEventListener(&amp;#x27;pointermove&amp;#x27;, (event) =&amp;gt; {
  mouse.x = event.clientX - bounds.left;
  mouse.y = event.clientY - bounds.top;
});

const distance = Math.hypot(dx, dy);
if (distance &amp;lt; radius) {
  particle.vx += (dx / distance) * force;
  particle.vy += (dy / distance) * force;
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
  &lt;p class=&quot;jh-exp-caption&quot;&gt;These two lines are the entire physics engine. The cursor position feeds the force; the damping factor controls how quickly particles settle. Change &lt;code&gt;0.93&lt;/code&gt; to &lt;code&gt;0.99&lt;/code&gt; for a much slower, dreamier decay.&lt;/p&gt;

  &lt;h2&gt;How to use this particle system in your HTML5 game&lt;/h2&gt;
  &lt;p&gt;This particle system is the building block for hit FX, explosions, magic effects, and environmental particles in any HTML5 game. When an enemy takes damage: spawn 20–40 particles at the hit position with a random velocity spread and their color set to the enemy&#39;s palette. When a spell lands: spawn 80+ particles with an outward spiral velocity. The demo uses the cursor as the spawn point — in your game, replace it with the collision point.&lt;/p&gt;
  &lt;p&gt;The core pattern — array of particles, physics update, draw, wrap — scales from 50 to 2 000 particles on mobile browsers. Euler integration (&lt;code&gt;position += velocity * dt&lt;/code&gt;), damping (&lt;code&gt;velocity *= 0.93&lt;/code&gt;), and boundary wrapping are the same three patterns used in every major game engine&#39;s CPU particle system.&lt;/p&gt;

  &lt;div class=&quot;jh-article-current&quot;&gt;
    &lt;strong&gt;Performance note&lt;/strong&gt;
    &lt;p&gt;520 particles at 60 fps is about 31 000 vector operations per second—well within Canvas 2D limits. Cap &lt;code&gt;devicePixelRatio&lt;/code&gt; at 2 to avoid doubling work on Retina screens. If you scale past 2 000 particles, switch to a &lt;code&gt;Float32Array&lt;/code&gt; for positions and profile on mobile before shipping.&lt;/p&gt;
  &lt;/div&gt;

  &lt;h2&gt;Extending this into game effects&lt;/h2&gt;
  &lt;p&gt;Replace repulsion with attraction to create a gravity vortex — perfect for a black hole enemy or a vacuum power-up. Add color tiers based on particle speed: fast = bright orange (fire), slow = dark red (embers) for a fire particle effect. Add a &lt;code&gt;life&lt;/code&gt; counter and fade alpha over time for explosion debris that disappears. Add gravity (&lt;code&gt;vy += 0.1&lt;/code&gt; each frame) for sparks that fall to the floor. These four tweaks turn this base system into four distinct game effects.&lt;/p&gt;

  &lt;h2&gt;Frequently asked questions&lt;/h2&gt;
  &lt;h3&gt;Can I use this particle cursor effect on a production website?&lt;/h3&gt;
  &lt;p&gt;Yes. The core is under 2 KB of vanilla JS with no build step required. Wrap the animation loop in a check for &lt;code&gt;window.matchMedia(&#39;(prefers-reduced-motion: reduce)&#39;)&lt;/code&gt; and skip it for users who prefer less motion—an essential accessibility step for any animated canvas.&lt;/p&gt;
  &lt;h3&gt;How do I make the particles attract instead of repel the cursor?&lt;/h3&gt;
  &lt;p&gt;Change the impulse direction: multiply &lt;code&gt;dx&lt;/code&gt; and &lt;code&gt;dy&lt;/code&gt; by &lt;code&gt;-1&lt;/code&gt; before adding to velocity. Now particles accelerate toward the cursor. Combine with a damping factor above 0.95 for a sticky cluster that slowly falls back into place when the cursor leaves.&lt;/p&gt;

  &lt;h2&gt;Related experiments&lt;/h2&gt;
  &lt;ul&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-magnetic-button-effect.html&quot;&gt;JavaScript Magnetic Button Effect&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/threejs-particle-cursor-effect.html&quot;&gt;Three.js Particle Cursor Effect&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/2026/09/javascript-neon-mouse-trail.html&quot;&gt;JavaScript Neon Mouse Trail Animation&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;

  &lt;div class=&quot;jh-article-end&quot;&gt;
    &lt;p&gt;This is part of a weekly series on visual JavaScript, modern CSS, browser graphics, and HTML5 games—one live experiment per post, copy-paste ready code, and the reasoning that makes each technique reusable beyond the demo.&lt;/p&gt;
  &lt;/div&gt;
&lt;/article&gt;</description><link>http://juegosenhtml5.blogspot.com/2026/09/javascript-particle-cursor-effect-canvas.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-3054964444681060645</guid><pubDate>Sun, 30 Aug 2026 13:35:33 +0000</pubDate><atom:updated>2026-08-30T08:37:02.936-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">black hole animation JavaScript</category><category domain="http://www.blogger.com/atom/ns#">Canvas accretion disk</category><category domain="http://www.blogger.com/atom/ns#">Canvas particle animation</category><category domain="http://www.blogger.com/atom/ns#">HTML5 Canvas animation</category><category domain="http://www.blogger.com/atom/ns#">JavaScript black hole animation</category><category domain="http://www.blogger.com/atom/ns#">JavaScript black hole effect</category><category domain="http://www.blogger.com/atom/ns#">JavaScript gravity simulation</category><category domain="http://www.blogger.com/atom/ns#">lang-en</category><title>JavaScript Black Hole Animation with Canvas</title><description>&lt;!--
BLOGGER POST — EN
Title / H1: JavaScript Black Hole Animation with Canvas
Suggested slug: javascript-black-hole-animation-canvas
Search description: Build an interactive black hole with JavaScript and Canvas, featuring an accretion disk, spiral particles, adjustable gravity, and a live demo.
Primary keyword: JavaScript black hole animation
Secondary keywords: black hole animation JavaScript, JavaScript black hole effect, HTML5 Canvas animation, Canvas particle animation, JavaScript gravity simulation
Labels: lang-en, en-experiments, JavaScript, Canvas, Animation, pair-black-hole-gravity
Series: Can JavaScript Do This?
Social title: I Built a Black Hole in JavaScript
Thumbnail text: JUST JS?
IMPORTANT: paste this content in Blogger HTML view. Set the post title and Search Description using the values above. Do not switch back to Compose view after inserting the script.
--&gt;
&lt;div class=&quot;jh-exp&quot; data-project=&quot;black-hole-gravity&quot; lang=&quot;en&quot;&gt;
  &lt;p class=&quot;jh-article-lead&quot;&gt;&lt;strong&gt;In this experiment, we&#39;ll build an interactive black hole animation with JavaScript and HTML5 Canvas&lt;/strong&gt;, including an accretion disk, spiraling particles, adjustable gravity, and a visual approximation of gravitational lensing. You can run the demo directly in your browser and change gravity, mass, and density in real time.&lt;/p&gt;

  &lt;section class=&quot;jh-exp-stage&quot; aria-label=&quot;Interactive Black Hole Demo&quot;&gt;
    &lt;div class=&quot;jh-exp-stage-head&quot;&gt;
      &lt;div&gt;&lt;span class=&quot;jh-exp-kicker&quot;&gt;LIVE DEMO&lt;/span&gt;&lt;h2&gt;Interactive Black Hole Demo&lt;/h2&gt;&lt;/div&gt;
      &lt;span class=&quot;jh-exp-hint&quot;&gt;Move the pointer near the black hole&lt;/span&gt;
    &lt;/div&gt;
    &lt;div class=&quot;jh-exp-canvas-wrap&quot;&gt;
      &lt;canvas class=&quot;jh-exp-canvas&quot; aria-label=&quot;Interactive black hole gravity demo&quot;&gt;&lt;/canvas&gt;
      &lt;div class=&quot;jh-exp-orbit-label&quot;&gt;CANVAS + JAVASCRIPT&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;jh-exp-controls&quot;&gt;
      &lt;label&gt;&lt;span&gt;Gravity &lt;b data-value=&quot;gravity&quot;&gt;0.55&lt;/b&gt;&lt;/span&gt;&lt;input data-control=&quot;gravity&quot; min=&quot;0.08&quot; max=&quot;1.4&quot; step=&quot;0.01&quot; type=&quot;range&quot; value=&quot;0.55&quot;/&gt;&lt;/label&gt;
      &lt;label&gt;&lt;span&gt;Mass &lt;b data-value=&quot;mass&quot;&gt;6.0&lt;/b&gt;&lt;/span&gt;&lt;input data-control=&quot;mass&quot; min=&quot;1&quot; max=&quot;14&quot; step=&quot;0.5&quot; type=&quot;range&quot; value=&quot;6&quot;/&gt;&lt;/label&gt;
      &lt;label&gt;&lt;span&gt;Particles &lt;b data-value=&quot;particles&quot;&gt;90&lt;/b&gt;&lt;/span&gt;&lt;input data-control=&quot;particles&quot; min=&quot;30&quot; max=&quot;180&quot; step=&quot;10&quot; type=&quot;range&quot; value=&quot;90&quot;/&gt;&lt;/label&gt;
      &lt;button class=&quot;jh-exp-reset&quot; data-action=&quot;reset&quot; type=&quot;button&quot;&gt;↻ Reset&lt;/button&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;section class=&quot;jh-article-current&quot;&gt;
    &lt;strong&gt;The idea in one line&lt;/strong&gt;
    &lt;p&gt;We are not solving general relativity in the browser. We combine a simple inverse-distance force with real visual cues — shadow, accretion disk, gravitational lensing and asymmetric brightness — so the result looks and feels much more convincing.&lt;/p&gt;
  &lt;/section&gt;

  &lt;h2&gt;How the JavaScript Gravity Effect Works&lt;/h2&gt;
  &lt;div class=&quot;jh-exp-codebox&quot;&gt;
    &lt;div class=&quot;jh-exp-code-head&quot;&gt;&lt;span&gt;gravity.js&lt;/span&gt;&lt;button data-action=&quot;copy&quot; data-copied=&quot;Copied&quot; type=&quot;button&quot;&gt;Copy code&lt;/button&gt;&lt;/div&gt;
    &lt;pre&gt;&lt;code data-code-source&gt;const dx = blackHole.x - particle.x;
const dy = blackHole.y - particle.y;
const distanceSq = dx * dx + dy * dy;

const force = (gravity * mass) / distanceSq;
const distance = Math.sqrt(distanceSq);
const nx = dx / distance;
const ny = dy / distance;

// Pull inward + add a tangential component so matter spirals.
particle.vx += nx * force - ny * orbit;
particle.vy += ny * force + nx * orbit;&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
  &lt;p class=&quot;jh-exp-caption&quot;&gt;The force stays intentionally simple; most of the visual realism comes from how the disk is projected, lit and bent around the shadow.&lt;/p&gt;

  &lt;h2&gt;How the Black Hole Effect Is Built&lt;/h2&gt;
  &lt;div class=&quot;jh-article-grid&quot;&gt;
    &lt;div class=&quot;jh-article-card&quot;&gt;&lt;strong&gt;1. Shadow + ring&lt;/strong&gt;&lt;span&gt;The center stays completely dark while a thin light ring marks the strongly lensed region around the shadow.&lt;/span&gt;&lt;/div&gt;
    &lt;div class=&quot;jh-article-card&quot;&gt;&lt;strong&gt;2. Accretion disk&lt;/strong&gt;&lt;span&gt;Hot gas orbits in an inclined disk: whiter near the center and more orange/red farther out.&lt;/span&gt;&lt;/div&gt;
    &lt;div class=&quot;jh-article-card&quot;&gt;&lt;strong&gt;3. Lensing + motion&lt;/strong&gt;&lt;span&gt;The back side of the disk reappears bent above and below the shadow while outer particles accelerate and spiral inward.&lt;/span&gt;&lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;
    &lt;div&gt;&lt;strong&gt;Challenge&lt;/strong&gt;&lt;p&gt;Try maximum mass with low gravity. Then reverse the radial force: the disk and lensing remain, but the outer particles begin to escape.&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-article-end&quot;&gt;This experiment is part of &lt;strong&gt;Can JavaScript Do This?&lt;/strong&gt;, our series of visual effects, games, and experiments built directly in the browser. &lt;strong&gt;Try the demo, change gravity, and watch how the system responds.&lt;/strong&gt;&lt;/div&gt;
  &lt;script&gt;
(function(){
  var root=document.currentScript.closest(&#39;.jh-exp&#39;);
  if(!root)return;
  var canvas=root.querySelector(&#39;.jh-exp-canvas&#39;);
  var ctx=canvas.getContext(&#39;2d&#39;);
  var gravityInput=root.querySelector(&#39;[data-control=&quot;gravity&quot;]&#39;);
  var massInput=root.querySelector(&#39;[data-control=&quot;mass&quot;]&#39;);
  var particlesInput=root.querySelector(&#39;[data-control=&quot;particles&quot;]&#39;);
  var resetBtn=root.querySelector(&#39;[data-action=&quot;reset&quot;]&#39;);
  var copyBtn=root.querySelector(&#39;[data-action=&quot;copy&quot;]&#39;);
  var code=root.querySelector(&#39;[data-code-source]&#39;);
  var gravityOut=root.querySelector(&#39;[data-value=&quot;gravity&quot;]&#39;);
  var massOut=root.querySelector(&#39;[data-value=&quot;mass&quot;]&#39;);
  var particlesOut=root.querySelector(&#39;[data-value=&quot;particles&quot;]&#39;);
  var dpr=Math.min(window.devicePixelRatio||1,2);
  var W=0,H=0,baseCx=0,baseCy=0,cx=0,cy=0,last=0,time=0;
  var pointer={x:0,y:0,active:false};
  var stars=[],gas=[],infall=[];

  function ga(name,params){
    try{if(window.gtag)window.gtag(&#39;event&#39;,name,params||{});}catch(e){}
  }
  function clamp(v,a,b){return Math.max(a,Math.min(b,v))}
  function rand(a,b){return a+Math.random()*(b-a)}
  function smoothstep(a,b,v){v=clamp((v-a)/(b-a),0,1);return v*v*(3-2*v)}
  function resize(){
    var r=canvas.getBoundingClientRect();
    W=Math.max(320,r.width);H=Math.max(300,r.height);
    canvas.width=Math.round(W*dpr);canvas.height=Math.round(H*dpr);
    ctx.setTransform(dpr,0,0,dpr,0,0);
    baseCx=W*.5;baseCy=H*.51;cx=baseCx;cy=baseCy;
    buildScene();
  }
  function shadowRadius(){
    var m=+massInput.value;
    return Math.min(W,H)*(0.092 + (m-1)/13*0.028);
  }
  function spawnInfallParticle(){
    var a = Math.random() * Math.PI * 2;
    var rr = rand(Math.min(W, H) * 0.46, Math.max(W, H) * 0.74);

    var radialSpeed = rand(0.02, 0.09);
    var tangentialSpeed = rand(0.16, 0.34);

    var x = cx + Math.cos(a) * rr;
    var y = cy + Math.sin(a) * rr;

    return {
      x: x,
      y: y,
      px: x,
      py: y,
      vx: -Math.cos(a) * radialSpeed - Math.sin(a) * tangentialSpeed,
      vy: -Math.sin(a) * radialSpeed + Math.cos(a) * tangentialSpeed,
      size: rand(0.55, 1.35),
      hue: rand(30, 44),
      alpha: rand(0.18, 0.48),
      spin: 1,
      curl: rand(0.75, 1.15),
      brightness: rand(0.85, 1.15)
    };
  }
  function buildScene(){
    stars=[];gas=[];infall=[];
    for(var i=0;i&lt;150;i++){
      stars.push({x:Math.random()*W,y:Math.random()*H,size:rand(.45,1.55),a:rand(.18,.85),tw:rand(0,6.28)});
    }
    var n=+particlesInput.value;
    for(var j=0;j&lt;n*2.5;j++){
      var u=Math.random();
      gas.push({r:Math.pow(u,.72),a:Math.random()*Math.PI*2,speed:rand(.42,1.15),size:rand(.7,2.35),band:rand(-1,1),phase:rand(0,Math.PI*2),alpha:rand(.25,.9)});
    }
    for(var k = 0; k &lt; Math.max(18, Math.round(n * 0.28)); k++){
      infall.push(spawnInfallParticle());
    }
    particlesOut.textContent=n;
  }
  function values(){
    gravityOut.textContent=(+gravityInput.value).toFixed(2);
    massOut.textContent=(+massInput.value).toFixed(1);
    particlesOut.textContent=particlesInput.value;
  }
  function background(){
    var r=shadowRadius();
    var bg=ctx.createRadialGradient(cx,cy,r*.2,cx,cy,Math.max(W,H)*.78);
    bg.addColorStop(0,&#39;#020307&#39;);bg.addColorStop(.18,&#39;#050912&#39;);bg.addColorStop(.48,&#39;#07131d&#39;);bg.addColorStop(1,&#39;#02070c&#39;);
    ctx.fillStyle=bg;ctx.fillRect(0,0,W,H);

    // Visual lensing cue: stars close to the shadow are stretched around it.
    for(var i=0;i&lt;stars.length;i++){
      var s=stars[i],dx=s.x-cx,dy=s.y-cy,d=Math.sqrt(dx*dx+dy*dy)+.001;
      var influence=1-smoothstep(r*1.35,r*4.8,d);
      var bend=influence*influence*r*.48;
      var nx=dx/d,ny=dy/d;
      var x=s.x+nx*bend,y=s.y+ny*bend;
      var tang=influence*r*.10;
      var twinkle=.72+.28*Math.sin(time*.0012+s.tw);
      if(influence&gt;.1){
        ctx.strokeStyle=&#39;rgba(195,226,255,&#39;+(s.a*influence*.38)+&#39;)&#39;;ctx.lineWidth=Math.max(.45,s.size*.55);
        ctx.beginPath();ctx.moveTo(x-ny*tang,y+nx*tang);ctx.lineTo(x+ny*tang,y-nx*tang);ctx.stroke();
      }
      ctx.fillStyle=&#39;rgba(235,246,255,&#39;+(s.a*twinkle)+&#39;)&#39;;ctx.beginPath();ctx.arc(x,y,s.size,0,Math.PI*2);ctx.fill();
    }

    var halo=ctx.createRadialGradient(cx,cy,r*.85,cx,cy,r*6.2);
    halo.addColorStop(0,&#39;rgba(255,112,20,.08)&#39;);halo.addColorStop(.23,&#39;rgba(255,68,8,.035)&#39;);halo.addColorStop(.55,&#39;rgba(13,68,100,.045)&#39;);halo.addColorStop(1,&#39;rgba(0,0,0,0)&#39;);
    ctx.fillStyle=halo;ctx.fillRect(0,0,W,H);
  }
  function gasColor(norm,boost,alpha){
    var h=18+norm*24;
    var l=82-norm*38;
    return &#39;hsla(&#39;+h+&#39;,100%,&#39;+clamp(l+boost*13,30,92)+&#39;%,&#39;+alpha+&#39;)&#39;;
  }
  function projectGas(p,angleOffset){
    var sr=shadowRadius();
    var outer=sr*5.0;
    var inner=sr*1.48;
    var radius=inner+(outer-inner)*p.r;
    var a=p.a+angleOffset;
    var inc=.80;
    var x=Math.cos(a)*radius;
    var z=Math.sin(a)*radius;
    var thickness=p.band*(2.1+7*p.r)+Math.sin(a*3+p.phase+time*.0012)*1.6;
    var y=z*Math.cos(inc)+thickness;
    var depth=z*Math.sin(inc);
    var perspective=1-depth/(outer*7.5);
    return {x:cx+x*perspective,y:cy+y*perspective,depth:depth,a:a};
  }
  function drawGasPass(farSide,lensed){
    var G=+gravityInput.value,M=+massInput.value;
    var orbitRate=(.000055+.000055*G)*Math.sqrt(M/6);
    var sr=shadowRadius();
    ctx.save();ctx.globalCompositeOperation=&#39;lighter&#39;;
    for(var i=0;i&lt;gas.length;i++){
      var p=gas[i];
      var q=projectGas(p,time*orbitRate*p.speed);
      var far=q.depth&gt;0;
      if(far!==farSide)continue;
      var x=q.x,y=q.y;
      var dx=x-cx,dy=y-cy,d=Math.sqrt(dx*dx+dy*dy)+.001;
      var lens=1-smoothstep(sr*1.2,sr*3.6,d);
      if(lensed){
        if(!far || lens&lt;.06)continue;
        var side=(dy&lt;0?-1:1);
        y=cy+side*(sr*1.03+Math.abs(dy)*.30)+Math.sin(q.a*2)*lens*sr*.10;
        x=cx+dx*(1+.22*lens);
      }else if(far &amp;&amp; lens&gt;.7){
        continue;
      }
      // Approximate relativistic beaming: one side of the disk is brighter.
      var approaching=(Math.cos(q.a-.25)+1)*.5;
      var boost=.22+.78*approaching;
      if(farSide)boost*=.78;
      if(lensed)boost*=.72;
      var alpha=p.alpha*(.18+.82*boost)*(lensed?lens:.78);
      var tangentX=-Math.sin(q.a),tangentY=Math.cos(q.a)*Math.cos(.80);
      var streak=(2.5+12*(1-p.r))*p.speed*(.55+G*.65);
      ctx.strokeStyle=gasColor(p.r,boost,alpha);
      ctx.lineWidth=Math.max(.55,p.size*(1.18-p.r*.36));
      ctx.beginPath();ctx.moveTo(x-tangentX*streak,y-tangentY*streak);ctx.lineTo(x+tangentX*streak*.25,y+tangentY*streak*.25);ctx.stroke();
      if((i%5)===0){ctx.fillStyle=gasColor(p.r,boost,alpha*.62);ctx.beginPath();ctx.arc(x,y,Math.max(.45,p.size*.55),0,Math.PI*2);ctx.fill();}
    }
    ctx.restore();
  }
  function drawDiskGlow(){
    var r=shadowRadius();
    ctx.save();ctx.translate(cx,cy);ctx.rotate(-.035);
    var g=ctx.createRadialGradient(0,0,r*1.2,0,0,r*5.4);
    g.addColorStop(0,&#39;rgba(255,230,155,.16)&#39;);g.addColorStop(.22,&#39;rgba(255,151,36,.15)&#39;);g.addColorStop(.52,&#39;rgba(255,69,9,.065)&#39;);g.addColorStop(1,&#39;rgba(255,40,0,0)&#39;);
    ctx.scale(1,.30);ctx.fillStyle=g;ctx.beginPath();ctx.arc(0,0,r*5.4,0,Math.PI*2);ctx.fill();ctx.restore();
  }
  function drawAccretionRibbon(farSide){
    var r=shadowRadius();
    var y=cy+(farSide?-r*.075:r*.105);
    var radii=[1.82,2.45,3.25,4.18];
    var widths=[r*.14,r*.105,r*.075,r*.048];
    var alphas=[.42,.28,.17,.09];
    var start=farSide?Math.PI:0,end=farSide?Math.PI*2:Math.PI;
    ctx.save();ctx.globalCompositeOperation=&#39;lighter&#39;;ctx.lineCap=&#39;round&#39;;
    for(var j=0;j&lt;radii.length;j++){
      var rr=r*radii[j];
      ctx.strokeStyle=&#39;rgba(255,&#39;+(150-j*18)+&#39;,&#39;+(42-j*7)+&#39;,&#39;+(alphas[j]*(farSide?.62:1))+&#39;)&#39;;
      ctx.lineWidth=widths[j];
      ctx.beginPath();ctx.ellipse(cx,y,rr,rr*(.20+j*.016),-.035,start,end);ctx.stroke();
      // Doppler-beaming cue: the approaching side is much brighter.
      ctx.strokeStyle=&#39;rgba(255,232,170,&#39;+(alphas[j]*(farSide?.70:1)*.78)+&#39;)&#39;;
      ctx.lineWidth=widths[j]*.48;
      var b0=farSide?Math.PI*1.46:Math.PI*.46;
      var b1=farSide?Math.PI*1.98:Math.PI*.98;
      ctx.beginPath();ctx.ellipse(cx,y,rr,rr*(.20+j*.016),-.035,b0,b1);ctx.stroke();
    }
    ctx.restore();
  }
  function drawDiskBands(farSide){
    var r=shadowRadius(),G=+gravityInput.value;
    ctx.save();ctx.lineCap=&#39;round&#39;;
    var rings=8,segs=64;
    ctx.globalCompositeOperation=&#39;lighter&#39;;
    var diskY=cy+(farSide?-r*.075:r*.105);
    for(var j=0;j&lt;rings;j++){
      var u=j/(rings-1);
      var radius=r*(1.56+u*3.55);
      var flatten=.235+u*.055;
      var baseAlpha=(1-u)*.33+.035;
      for(var i=0;i&lt;segs;i++){
        var a0=i/segs*Math.PI*2;
        var a1=(i+1.20)/segs*Math.PI*2;
        var s0=Math.sin(a0);
        var isFar=s0&lt;0;
        if(isFar!==farSide)continue;
        var approach=(Math.cos(a0-.18)+1)*.5;
        var beam=.28+.72*approach;
        var alpha=baseAlpha*(farSide?.62:1)*(0.50+.82*beam);
        var hue=18+u*20;
        var light=86-u*40+beam*8;
        ctx.strokeStyle=&#39;hsla(&#39;+hue+&#39;,100%,&#39;+clamp(light,38,94)+&#39;%,&#39;+alpha+&#39;)&#39;;
        ctx.lineWidth=(1-u)*2.55+.55;
        ctx.beginPath();
        ctx.ellipse(cx,diskY,radius,radius*flatten,-.035,a0,a1);
        ctx.stroke();
      }
    }
    // A hot inner rim makes the accretion disk immediately readable.
    var inner=r*1.62;
    ctx.strokeStyle=farSide?&#39;rgba(255,151,40,.50)&#39;:&#39;rgba(255,236,173,.88)&#39;;
    ctx.lineWidth=farSide?2.2:3.2;
    ctx.beginPath();ctx.ellipse(cx,diskY,inner,inner*.235,-.035,farSide?Math.PI:0,farSide?Math.PI*2:Math.PI);ctx.stroke();
    ctx.restore();
  }
  function drawShadowAndPhotonRing(){
    var r=shadowRadius();
    var sh=ctx.createRadialGradient(cx-r*.18,cy-r*.16,r*.12,cx,cy,r*1.20);
    sh.addColorStop(0,&#39;rgba(0,0,0,1)&#39;);sh.addColorStop(.78,&#39;rgba(0,0,0,1)&#39;);sh.addColorStop(1,&#39;rgba(0,0,0,.93)&#39;);
    ctx.fillStyle=sh;ctx.beginPath();ctx.arc(cx,cy,r*1.14,0,Math.PI*2);ctx.fill();

    ctx.save();ctx.lineCap=&#39;round&#39;;
    var segs=96;
    for(var i=0;i&lt;segs;i++){
      var a0=i/segs*Math.PI*2,a1=(i+1.35)/segs*Math.PI*2;
      var beam=.20+.80*((Math.cos(a0-.35)+1)*.5);
      ctx.strokeStyle=&#39;rgba(255,&#39;+Math.round(145+95*beam)+&#39;,&#39;+Math.round(45+150*beam)+&#39;,&#39;+(.30+.62*beam)+&#39;)&#39;;
      ctx.lineWidth=1.15+2.55*beam;
      ctx.beginPath();ctx.arc(cx,cy,r*1.24,a0,a1);ctx.stroke();
    }
    ctx.restore();
    ctx.strokeStyle=&#39;rgba(255,205,121,.18)&#39;;ctx.lineWidth=.8;ctx.beginPath();ctx.arc(cx,cy,r*1.34,0,Math.PI*2);ctx.stroke();
  }
  function drawInfall(dt){
    var G = +gravityInput.value;
    var M = +massInput.value;
    var sr = shadowRadius();

    ctx.save();
    ctx.lineCap = &#39;round&#39;;
    ctx.lineJoin = &#39;round&#39;;
    ctx.globalCompositeOperation = &#39;lighter&#39;;

    for(var i = 0; i &lt; infall.length; i++){
      var p = infall[i];

      p.px = p.x;
      p.py = p.y;

      var dx = cx - p.x;
      var dy = cy - p.y;
      var d2 = dx * dx + dy * dy;
      var d = Math.sqrt(d2) + 0.001;

      var nx = dx / d;
      var ny = dy / d;

      var tx = -ny * p.spin;
      var ty =  nx * p.spin;

      var pull = (G * M * 420) / Math.max(sr * sr * 0.65, d2);

      var near = smoothstep(sr * 7.5, sr * 1.35, d);
      var swirl = 0.0036 * (0.65 + G * 0.9) * Math.sqrt(M / 6) * near * p.curl;

      var drag = Math.pow(0.992, dt / 16.67);

      p.vx = (p.vx + (nx * pull + tx * swirl) * dt * 0.055) * drag;
      p.vy = (p.vy + (ny * pull + ty * swirl) * dt * 0.055) * drag;

      p.x += p.vx * dt * 0.060;
      p.y += p.vy * dt * 0.060;

      if(
        d &lt; sr * 1.18 ||
        p.x &lt; -120 || p.x &gt; W + 120 ||
        p.y &lt; -120 || p.y &gt; H + 120
      ){
        infall[i] = spawnInfallParticle();
        continue;
      }

      var mvx = p.x - p.px;
      var mvy = p.y - p.py;
      var speed = Math.hypot(mvx, mvy);

      if(speed &lt; 0.001) continue;

      var inv = 1 / speed;
      var dirX = mvx * inv;
      var dirY = mvy * inv;

      var trailLen = clamp(4 + speed * 1.5 + near * 6, 4, 15);

      var curve = (1 - near) * 1.5 + near * (5.5 + speed * 1.2);
      var curveX = -dirY * curve * p.spin;
      var curveY =  dirX * curve * p.spin;

      var alpha = clamp(p.alpha * (0.45 + near * 0.9), 0.12, 0.68);
      var width = clamp(p.size * (0.85 + near * 0.65), 0.55, 1.9);

      ctx.strokeStyle = &#39;hsla(&#39; + p.hue + &#39;,100%,&#39; + (66 + near * 18) + &#39;%,&#39; + (alpha * 0.42) + &#39;)&#39;;
      ctx.lineWidth = width * 1.85;
      ctx.beginPath();
      ctx.moveTo(
        p.px - dirX * trailLen * 0.55,
        p.py - dirY * trailLen * 0.55
      );
      ctx.quadraticCurveTo(
        p.px - dirX * trailLen + curveX * 0.35,
        p.py - dirY * trailLen + curveY * 0.35,
        p.x,
        p.y
      );
      ctx.stroke();

      ctx.strokeStyle = &#39;hsla(&#39; + (p.hue + 6) + &#39;,100%,&#39; + (78 + near * 12) + &#39;%,&#39; + alpha + &#39;)&#39;;
      ctx.lineWidth = width;
      ctx.beginPath();
      ctx.moveTo(
        p.px - dirX * trailLen * 0.35,
        p.py - dirY * trailLen * 0.35
      );
      ctx.quadraticCurveTo(
        p.px - dirX * trailLen * 0.65 + curveX * 0.22,
        p.py - dirY * trailLen * 0.65 + curveY * 0.22,
        p.x,
        p.y
      );
      ctx.stroke();

      ctx.fillStyle = &#39;rgba(255,238,190,&#39; + clamp(alpha * 0.95, 0.16, 0.78) + &#39;)&#39;;
      ctx.beginPath();
      ctx.arc(p.x, p.y, Math.max(0.55, width * 0.72), 0, Math.PI * 2);
      ctx.fill();
    }

    ctx.restore();
  }
  function drawLensingArcs(){
    var r=shadowRadius();
    ctx.save();ctx.lineCap=&#39;round&#39;;
    var pulse=.90+.10*Math.sin(time*.0017);
    ctx.strokeStyle=&#39;rgba(255,126,28,&#39;+(.42*pulse)+&#39;)&#39;;ctx.lineWidth=r*.13;
    ctx.beginPath();ctx.ellipse(cx,cy-r*.10,r*1.62,r*1.48,0,Math.PI*1.08,Math.PI*1.92);ctx.stroke();
    ctx.strokeStyle=&#39;rgba(255,220,145,&#39;+(.24*pulse)+&#39;)&#39;;ctx.lineWidth=r*.070;
    ctx.beginPath();ctx.ellipse(cx,cy+r*.13,r*1.48,r*1.35,0,.10*Math.PI,.90*Math.PI);ctx.stroke();
    ctx.restore();
  }
  function tick(now){
    var dt=last?Math.min(34,now-last):16.67;last=now;time=now;
    var px=pointer.active?(pointer.x-W*.5):0,py=pointer.active?(pointer.y-H*.5):0;
    var tx=baseCx+clamp(px*.018,-W*.014,W*.014),ty=baseCy+clamp(py*.012,-H*.012,H*.012);
    cx+=(tx-cx)*.055;cy+=(ty-cy)*.055;

    background();
    drawDiskGlow();
    drawAccretionRibbon(true);
    drawDiskBands(true);
    drawGasPass(true,false);
    drawGasPass(true,true);
    drawLensingArcs();
    drawShadowAndPhotonRing();
    drawAccretionRibbon(false);
    drawDiskBands(false);
    drawGasPass(false,false);
    drawInfall(dt);
    requestAnimationFrame(tick);
  }
  function pos(e){
    var r=canvas.getBoundingClientRect();pointer.x=e.clientX-r.left;pointer.y=e.clientY-r.top;pointer.active=true;
  }
  canvas.addEventListener(&#39;pointermove&#39;,pos);
  canvas.addEventListener(&#39;pointerenter&#39;,function(e){pos(e);ga(&#39;demo_interaction&#39;,{project:&#39;black-hole-gravity&#39;,kind:&#39;pointer&#39;});},{once:true});
  canvas.addEventListener(&#39;pointerleave&#39;,function(){pointer.active=false;});
  [gravityInput,massInput].forEach(function(el){el.addEventListener(&#39;input&#39;,function(){values();ga(&#39;parameter_change&#39;,{project:&#39;black-hole-gravity&#39;,parameter:el.dataset.control,value:el.value});});});
  particlesInput.addEventListener(&#39;change&#39;,function(){values();buildScene();ga(&#39;parameter_change&#39;,{project:&#39;black-hole-gravity&#39;,parameter:&#39;particles&#39;,value:this.value});});
  resetBtn.addEventListener(&#39;click&#39;,function(){gravityInput.value=.55;massInput.value=6;particlesInput.value=90;values();buildScene();ga(&#39;demo_reset&#39;,{project:&#39;black-hole-gravity&#39;,particles:90});});
  copyBtn.addEventListener(&#39;click&#39;,function(){
    var txt=code.textContent;
    if(navigator.clipboard)navigator.clipboard.writeText(txt).then(function(){var old=copyBtn.textContent;copyBtn.textContent=copyBtn.dataset.copied;setTimeout(function(){copyBtn.textContent=old},1200);ga(&#39;code_copy&#39;,{project:&#39;black-hole-gravity&#39;});});
  });
  window.addEventListener(&#39;resize&#39;,resize);
  resize();values();requestAnimationFrame(tick);
})();
&lt;/script&gt;
&lt;/div&gt;
</description><link>http://juegosenhtml5.blogspot.com/2026/08/javascript-black-hole-animation-canvas.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-5923338899789812113</guid><pubDate>Sun, 30 Aug 2026 13:31:52 +0000</pubDate><atom:updated>2026-08-30T08:37:59.619-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">agujero negro JavaScript</category><category domain="http://www.blogger.com/atom/ns#">animación Canvas JavaScript</category><category domain="http://www.blogger.com/atom/ns#">disco de acreción Canvas</category><category domain="http://www.blogger.com/atom/ns#">efecto agujero negro JavaScript</category><category domain="http://www.blogger.com/atom/ns#">HTML5 Canvas JavaScript</category><category domain="http://www.blogger.com/atom/ns#">lang-es</category><category domain="http://www.blogger.com/atom/ns#">partículas Canvas JavaScript</category><category domain="http://www.blogger.com/atom/ns#">simulación gravedad JavaScript</category><title>Cómo crear un agujero negro con JavaScript y Canvas</title><description>&lt;!--
BLOGGER POST — ES
Title: Cómo crear un agujero negro con JavaScript y Canvas
Suggested slug: agujero-negro-javascript-canvas
Labels: lang-es, Experimentos, JavaScript, Canvas, Animation, pair-black-hole-gravity
IMPORTANT: paste in Blogger HTML view and do not switch back to Compose view after inserting the script.
--&gt;
&lt;div class=&quot;jh-exp&quot; data-project=&quot;black-hole-gravity&quot; lang=&quot;es&quot;&gt;
  &lt;p class=&quot;jh-article-lead&quot;&gt;Una visualización interactiva inspirada en cómo reconocemos un agujero negro: sombra central, anillo de fotones, disco de acreción brillante y luz deformada alrededor. Mueve el cursor y cambia gravedad, masa y densidad para ver cómo responde la escena.&lt;/p&gt;

  &lt;section class=&quot;jh-exp-stage&quot; aria-label=&quot;Pruébalo&quot;&gt;
    &lt;div class=&quot;jh-exp-stage-head&quot;&gt;
      &lt;div&gt;&lt;span class=&quot;jh-exp-kicker&quot;&gt;LIVE DEMO&lt;/span&gt;&lt;h2&gt;Pruébalo&lt;/h2&gt;&lt;/div&gt;
      &lt;span class=&quot;jh-exp-hint&quot;&gt;Mueve el cursor cerca del agujero negro&lt;/span&gt;
    &lt;/div&gt;
    &lt;div class=&quot;jh-exp-canvas-wrap&quot;&gt;
      &lt;canvas class=&quot;jh-exp-canvas&quot; aria-label=&quot;Interactive black hole gravity demo&quot;&gt;&lt;/canvas&gt;
      &lt;div class=&quot;jh-exp-orbit-label&quot;&gt;CANVAS + JAVASCRIPT&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;jh-exp-controls&quot;&gt;
      &lt;label&gt;&lt;span&gt;Gravedad &lt;b data-value=&quot;gravity&quot;&gt;0.55&lt;/b&gt;&lt;/span&gt;&lt;input data-control=&quot;gravity&quot; min=&quot;0.08&quot; max=&quot;1.4&quot; step=&quot;0.01&quot; type=&quot;range&quot; value=&quot;0.55&quot;/&gt;&lt;/label&gt;
      &lt;label&gt;&lt;span&gt;Masa &lt;b data-value=&quot;mass&quot;&gt;6.0&lt;/b&gt;&lt;/span&gt;&lt;input data-control=&quot;mass&quot; min=&quot;1&quot; max=&quot;14&quot; step=&quot;0.5&quot; type=&quot;range&quot; value=&quot;6&quot;/&gt;&lt;/label&gt;
      &lt;label&gt;&lt;span&gt;Partículas &lt;b data-value=&quot;particles&quot;&gt;90&lt;/b&gt;&lt;/span&gt;&lt;input data-control=&quot;particles&quot; min=&quot;30&quot; max=&quot;180&quot; step=&quot;10&quot; type=&quot;range&quot; value=&quot;90&quot;/&gt;&lt;/label&gt;
      &lt;button class=&quot;jh-exp-reset&quot; data-action=&quot;reset&quot; type=&quot;button&quot;&gt;↻ Reiniciar&lt;/button&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;section class=&quot;jh-article-current&quot;&gt;
    &lt;strong&gt;La idea en una línea&lt;/strong&gt;
    &lt;p&gt;No intentamos resolver relatividad general en el navegador. Combinamos una fuerza inversa con señales visuales reales de un agujero negro —sombra, disco de acreción, lente gravitacional y brillo asimétrico— para que el resultado se vea y se sienta mucho más convincente.&lt;/p&gt;
  &lt;/section&gt;

  &lt;h2&gt;La línea que cambia todo&lt;/h2&gt;
  &lt;div class=&quot;jh-exp-codebox&quot;&gt;
    &lt;div class=&quot;jh-exp-code-head&quot;&gt;&lt;span&gt;gravity.js&lt;/span&gt;&lt;button data-action=&quot;copy&quot; data-copied=&quot;Copiado&quot; type=&quot;button&quot;&gt;Copiar código&lt;/button&gt;&lt;/div&gt;
    &lt;pre&gt;&lt;code data-code-source&gt;const dx = blackHole.x - particle.x;
const dy = blackHole.y - particle.y;
const distanceSq = dx * dx + dy * dy;

const force = (gravity * mass) / distanceSq;
const distance = Math.sqrt(distanceSq);
const nx = dx / distance;
const ny = dy / distance;

// Pull inward + add a tangential component so matter spirals.
particle.vx += nx * force - ny * orbit;
particle.vy += ny * force + nx * orbit;&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
  &lt;p class=&quot;jh-exp-caption&quot;&gt;La fuerza sigue siendo deliberadamente simple; el realismo visual viene de cómo proyectamos, iluminamos y curvamos el disco alrededor de la sombra.&lt;/p&gt;

  &lt;h2&gt;Qué está pasando&lt;/h2&gt;
  &lt;div class=&quot;jh-article-grid&quot;&gt;
    &lt;div class=&quot;jh-article-card&quot;&gt;&lt;strong&gt;1. Sombra + anillo&lt;/strong&gt;&lt;span&gt;El centro permanece completamente oscuro y un anillo fino de luz marca la zona donde la luz fuertemente curvada domina la imagen.&lt;/span&gt;&lt;/div&gt;
    &lt;div class=&quot;jh-article-card&quot;&gt;&lt;strong&gt;2. Disco de acreción&lt;/strong&gt;&lt;span&gt;El gas caliente orbita en un disco inclinado: más blanco cerca del centro y más naranja/rojo hacia afuera.&lt;/span&gt;&lt;/div&gt;
    &lt;div class=&quot;jh-article-card&quot;&gt;&lt;strong&gt;3. Lente + movimiento&lt;/strong&gt;&lt;span&gt;La parte trasera del disco reaparece curvada arriba y abajo, mientras las partículas externas aceleran y espiralan hacia el centro.&lt;/span&gt;&lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-article-note&quot;&gt;
    &lt;div class=&quot;jh-article-note-icon&quot;&gt;↗&lt;/div&gt;
    &lt;div&gt;&lt;strong&gt;Reto&lt;/strong&gt;&lt;p&gt;Prueba masa máxima y gravedad baja. Después invierte la fuerza radial: conservarás el disco y la lente, pero las partículas exteriores comenzarán a escapar.&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-article-end&quot;&gt;Este experimento forma parte de nuestra serie de cosas visuales hechas directamente en el navegador.&lt;/div&gt;
  &lt;script&gt;
(function(){
  var root=document.currentScript.closest(&#39;.jh-exp&#39;);
  if(!root)return;
  var canvas=root.querySelector(&#39;.jh-exp-canvas&#39;);
  var ctx=canvas.getContext(&#39;2d&#39;);
  var gravityInput=root.querySelector(&#39;[data-control=&quot;gravity&quot;]&#39;);
  var massInput=root.querySelector(&#39;[data-control=&quot;mass&quot;]&#39;);
  var particlesInput=root.querySelector(&#39;[data-control=&quot;particles&quot;]&#39;);
  var resetBtn=root.querySelector(&#39;[data-action=&quot;reset&quot;]&#39;);
  var copyBtn=root.querySelector(&#39;[data-action=&quot;copy&quot;]&#39;);
  var code=root.querySelector(&#39;[data-code-source]&#39;);
  var gravityOut=root.querySelector(&#39;[data-value=&quot;gravity&quot;]&#39;);
  var massOut=root.querySelector(&#39;[data-value=&quot;mass&quot;]&#39;);
  var particlesOut=root.querySelector(&#39;[data-value=&quot;particles&quot;]&#39;);
  var dpr=Math.min(window.devicePixelRatio||1,2);
  var W=0,H=0,baseCx=0,baseCy=0,cx=0,cy=0,last=0,time=0;
  var pointer={x:0,y:0,active:false};
  var stars=[],gas=[],infall=[];

  function ga(name,params){
    try{if(window.gtag)window.gtag(&#39;event&#39;,name,params||{});}catch(e){}
  }
  function clamp(v,a,b){return Math.max(a,Math.min(b,v))}
  function rand(a,b){return a+Math.random()*(b-a)}
  function smoothstep(a,b,v){v=clamp((v-a)/(b-a),0,1);return v*v*(3-2*v)}
  function resize(){
    var r=canvas.getBoundingClientRect();
    W=Math.max(320,r.width);H=Math.max(300,r.height);
    canvas.width=Math.round(W*dpr);canvas.height=Math.round(H*dpr);
    ctx.setTransform(dpr,0,0,dpr,0,0);
    baseCx=W*.5;baseCy=H*.51;cx=baseCx;cy=baseCy;
    buildScene();
  }
  function shadowRadius(){
    var m=+massInput.value;
    return Math.min(W,H)*(0.092 + (m-1)/13*0.028);
  }
  function spawnInfallParticle(){
    var a = Math.random() * Math.PI * 2;
    var rr = rand(Math.min(W, H) * 0.46, Math.max(W, H) * 0.74);

    var radialSpeed = rand(0.02, 0.09);
    var tangentialSpeed = rand(0.16, 0.34);

    var x = cx + Math.cos(a) * rr;
    var y = cy + Math.sin(a) * rr;

    return {
      x: x,
      y: y,
      px: x,
      py: y,
      vx: -Math.cos(a) * radialSpeed - Math.sin(a) * tangentialSpeed,
      vy: -Math.sin(a) * radialSpeed + Math.cos(a) * tangentialSpeed,
      size: rand(0.55, 1.35),
      hue: rand(30, 44),
      alpha: rand(0.18, 0.48),
      spin: 1,
      curl: rand(0.75, 1.15),
      brightness: rand(0.85, 1.15)
    };
  }
  function buildScene(){
    stars=[];gas=[];infall=[];
    for(var i=0;i&lt;150;i++){
      stars.push({x:Math.random()*W,y:Math.random()*H,size:rand(.45,1.55),a:rand(.18,.85),tw:rand(0,6.28)});
    }
    var n=+particlesInput.value;
    for(var j=0;j&lt;n*2.5;j++){
      var u=Math.random();
      gas.push({r:Math.pow(u,.72),a:Math.random()*Math.PI*2,speed:rand(.42,1.15),size:rand(.7,2.35),band:rand(-1,1),phase:rand(0,Math.PI*2),alpha:rand(.25,.9)});
    }
    for(var k = 0; k &lt; Math.max(18, Math.round(n * 0.28)); k++){
      infall.push(spawnInfallParticle());
    }
    particlesOut.textContent=n;
  }
  function values(){
    gravityOut.textContent=(+gravityInput.value).toFixed(2);
    massOut.textContent=(+massInput.value).toFixed(1);
    particlesOut.textContent=particlesInput.value;
  }
  function background(){
    var r=shadowRadius();
    var bg=ctx.createRadialGradient(cx,cy,r*.2,cx,cy,Math.max(W,H)*.78);
    bg.addColorStop(0,&#39;#020307&#39;);bg.addColorStop(.18,&#39;#050912&#39;);bg.addColorStop(.48,&#39;#07131d&#39;);bg.addColorStop(1,&#39;#02070c&#39;);
    ctx.fillStyle=bg;ctx.fillRect(0,0,W,H);

    // Visual lensing cue: stars close to the shadow are stretched around it.
    for(var i=0;i&lt;stars.length;i++){
      var s=stars[i],dx=s.x-cx,dy=s.y-cy,d=Math.sqrt(dx*dx+dy*dy)+.001;
      var influence=1-smoothstep(r*1.35,r*4.8,d);
      var bend=influence*influence*r*.48;
      var nx=dx/d,ny=dy/d;
      var x=s.x+nx*bend,y=s.y+ny*bend;
      var tang=influence*r*.10;
      var twinkle=.72+.28*Math.sin(time*.0012+s.tw);
      if(influence&gt;.1){
        ctx.strokeStyle=&#39;rgba(195,226,255,&#39;+(s.a*influence*.38)+&#39;)&#39;;ctx.lineWidth=Math.max(.45,s.size*.55);
        ctx.beginPath();ctx.moveTo(x-ny*tang,y+nx*tang);ctx.lineTo(x+ny*tang,y-nx*tang);ctx.stroke();
      }
      ctx.fillStyle=&#39;rgba(235,246,255,&#39;+(s.a*twinkle)+&#39;)&#39;;ctx.beginPath();ctx.arc(x,y,s.size,0,Math.PI*2);ctx.fill();
    }

    var halo=ctx.createRadialGradient(cx,cy,r*.85,cx,cy,r*6.2);
    halo.addColorStop(0,&#39;rgba(255,112,20,.08)&#39;);halo.addColorStop(.23,&#39;rgba(255,68,8,.035)&#39;);halo.addColorStop(.55,&#39;rgba(13,68,100,.045)&#39;);halo.addColorStop(1,&#39;rgba(0,0,0,0)&#39;);
    ctx.fillStyle=halo;ctx.fillRect(0,0,W,H);
  }
  function gasColor(norm,boost,alpha){
    var h=18+norm*24;
    var l=82-norm*38;
    return &#39;hsla(&#39;+h+&#39;,100%,&#39;+clamp(l+boost*13,30,92)+&#39;%,&#39;+alpha+&#39;)&#39;;
  }
  function projectGas(p,angleOffset){
    var sr=shadowRadius();
    var outer=sr*5.0;
    var inner=sr*1.48;
    var radius=inner+(outer-inner)*p.r;
    var a=p.a+angleOffset;
    var inc=.80;
    var x=Math.cos(a)*radius;
    var z=Math.sin(a)*radius;
    var thickness=p.band*(2.1+7*p.r)+Math.sin(a*3+p.phase+time*.0012)*1.6;
    var y=z*Math.cos(inc)+thickness;
    var depth=z*Math.sin(inc);
    var perspective=1-depth/(outer*7.5);
    return {x:cx+x*perspective,y:cy+y*perspective,depth:depth,a:a};
  }
  function drawGasPass(farSide,lensed){
    var G=+gravityInput.value,M=+massInput.value;
    var orbitRate=(.000055+.000055*G)*Math.sqrt(M/6);
    var sr=shadowRadius();
    ctx.save();ctx.globalCompositeOperation=&#39;lighter&#39;;
    for(var i=0;i&lt;gas.length;i++){
      var p=gas[i];
      var q=projectGas(p,time*orbitRate*p.speed);
      var far=q.depth&gt;0;
      if(far!==farSide)continue;
      var x=q.x,y=q.y;
      var dx=x-cx,dy=y-cy,d=Math.sqrt(dx*dx+dy*dy)+.001;
      var lens=1-smoothstep(sr*1.2,sr*3.6,d);
      if(lensed){
        if(!far || lens&lt;.06)continue;
        var side=(dy&lt;0?-1:1);
        y=cy+side*(sr*1.03+Math.abs(dy)*.30)+Math.sin(q.a*2)*lens*sr*.10;
        x=cx+dx*(1+.22*lens);
      }else if(far &amp;&amp; lens&gt;.7){
        continue;
      }
      // Approximate relativistic beaming: one side of the disk is brighter.
      var approaching=(Math.cos(q.a-.25)+1)*.5;
      var boost=.22+.78*approaching;
      if(farSide)boost*=.78;
      if(lensed)boost*=.72;
      var alpha=p.alpha*(.18+.82*boost)*(lensed?lens:.78);
      var tangentX=-Math.sin(q.a),tangentY=Math.cos(q.a)*Math.cos(.80);
      var streak=(2.5+12*(1-p.r))*p.speed*(.55+G*.65);
      ctx.strokeStyle=gasColor(p.r,boost,alpha);
      ctx.lineWidth=Math.max(.55,p.size*(1.18-p.r*.36));
      ctx.beginPath();ctx.moveTo(x-tangentX*streak,y-tangentY*streak);ctx.lineTo(x+tangentX*streak*.25,y+tangentY*streak*.25);ctx.stroke();
      if((i%5)===0){ctx.fillStyle=gasColor(p.r,boost,alpha*.62);ctx.beginPath();ctx.arc(x,y,Math.max(.45,p.size*.55),0,Math.PI*2);ctx.fill();}
    }
    ctx.restore();
  }
  function drawDiskGlow(){
    var r=shadowRadius();
    ctx.save();ctx.translate(cx,cy);ctx.rotate(-.035);
    var g=ctx.createRadialGradient(0,0,r*1.2,0,0,r*5.4);
    g.addColorStop(0,&#39;rgba(255,230,155,.16)&#39;);g.addColorStop(.22,&#39;rgba(255,151,36,.15)&#39;);g.addColorStop(.52,&#39;rgba(255,69,9,.065)&#39;);g.addColorStop(1,&#39;rgba(255,40,0,0)&#39;);
    ctx.scale(1,.30);ctx.fillStyle=g;ctx.beginPath();ctx.arc(0,0,r*5.4,0,Math.PI*2);ctx.fill();ctx.restore();
  }
  function drawAccretionRibbon(farSide){
    var r=shadowRadius();
    var y=cy+(farSide?-r*.075:r*.105);
    var radii=[1.82,2.45,3.25,4.18];
    var widths=[r*.14,r*.105,r*.075,r*.048];
    var alphas=[.42,.28,.17,.09];
    var start=farSide?Math.PI:0,end=farSide?Math.PI*2:Math.PI;
    ctx.save();ctx.globalCompositeOperation=&#39;lighter&#39;;ctx.lineCap=&#39;round&#39;;
    for(var j=0;j&lt;radii.length;j++){
      var rr=r*radii[j];
      ctx.strokeStyle=&#39;rgba(255,&#39;+(150-j*18)+&#39;,&#39;+(42-j*7)+&#39;,&#39;+(alphas[j]*(farSide?.62:1))+&#39;)&#39;;
      ctx.lineWidth=widths[j];
      ctx.beginPath();ctx.ellipse(cx,y,rr,rr*(.20+j*.016),-.035,start,end);ctx.stroke();
      // Doppler-beaming cue: the approaching side is much brighter.
      ctx.strokeStyle=&#39;rgba(255,232,170,&#39;+(alphas[j]*(farSide?.70:1)*.78)+&#39;)&#39;;
      ctx.lineWidth=widths[j]*.48;
      var b0=farSide?Math.PI*1.46:Math.PI*.46;
      var b1=farSide?Math.PI*1.98:Math.PI*.98;
      ctx.beginPath();ctx.ellipse(cx,y,rr,rr*(.20+j*.016),-.035,b0,b1);ctx.stroke();
    }
    ctx.restore();
  }
  function drawDiskBands(farSide){
    var r=shadowRadius(),G=+gravityInput.value;
    ctx.save();ctx.lineCap=&#39;round&#39;;
    var rings=8,segs=64;
    ctx.globalCompositeOperation=&#39;lighter&#39;;
    var diskY=cy+(farSide?-r*.075:r*.105);
    for(var j=0;j&lt;rings;j++){
      var u=j/(rings-1);
      var radius=r*(1.56+u*3.55);
      var flatten=.235+u*.055;
      var baseAlpha=(1-u)*.33+.035;
      for(var i=0;i&lt;segs;i++){
        var a0=i/segs*Math.PI*2;
        var a1=(i+1.20)/segs*Math.PI*2;
        var s0=Math.sin(a0);
        var isFar=s0&lt;0;
        if(isFar!==farSide)continue;
        var approach=(Math.cos(a0-.18)+1)*.5;
        var beam=.28+.72*approach;
        var alpha=baseAlpha*(farSide?.62:1)*(0.50+.82*beam);
        var hue=18+u*20;
        var light=86-u*40+beam*8;
        ctx.strokeStyle=&#39;hsla(&#39;+hue+&#39;,100%,&#39;+clamp(light,38,94)+&#39;%,&#39;+alpha+&#39;)&#39;;
        ctx.lineWidth=(1-u)*2.55+.55;
        ctx.beginPath();
        ctx.ellipse(cx,diskY,radius,radius*flatten,-.035,a0,a1);
        ctx.stroke();
      }
    }
    // A hot inner rim makes the accretion disk immediately readable.
    var inner=r*1.62;
    ctx.strokeStyle=farSide?&#39;rgba(255,151,40,.50)&#39;:&#39;rgba(255,236,173,.88)&#39;;
    ctx.lineWidth=farSide?2.2:3.2;
    ctx.beginPath();ctx.ellipse(cx,diskY,inner,inner*.235,-.035,farSide?Math.PI:0,farSide?Math.PI*2:Math.PI);ctx.stroke();
    ctx.restore();
  }
  function drawShadowAndPhotonRing(){
    var r=shadowRadius();
    var sh=ctx.createRadialGradient(cx-r*.18,cy-r*.16,r*.12,cx,cy,r*1.20);
    sh.addColorStop(0,&#39;rgba(0,0,0,1)&#39;);sh.addColorStop(.78,&#39;rgba(0,0,0,1)&#39;);sh.addColorStop(1,&#39;rgba(0,0,0,.93)&#39;);
    ctx.fillStyle=sh;ctx.beginPath();ctx.arc(cx,cy,r*1.14,0,Math.PI*2);ctx.fill();

    ctx.save();ctx.lineCap=&#39;round&#39;;
    var segs=96;
    for(var i=0;i&lt;segs;i++){
      var a0=i/segs*Math.PI*2,a1=(i+1.35)/segs*Math.PI*2;
      var beam=.20+.80*((Math.cos(a0-.35)+1)*.5);
      ctx.strokeStyle=&#39;rgba(255,&#39;+Math.round(145+95*beam)+&#39;,&#39;+Math.round(45+150*beam)+&#39;,&#39;+(.30+.62*beam)+&#39;)&#39;;
      ctx.lineWidth=1.15+2.55*beam;
      ctx.beginPath();ctx.arc(cx,cy,r*1.24,a0,a1);ctx.stroke();
    }
    ctx.restore();
    ctx.strokeStyle=&#39;rgba(255,205,121,.18)&#39;;ctx.lineWidth=.8;ctx.beginPath();ctx.arc(cx,cy,r*1.34,0,Math.PI*2);ctx.stroke();
  }
  function drawInfall(dt){
    var G = +gravityInput.value;
    var M = +massInput.value;
    var sr = shadowRadius();

    ctx.save();
    ctx.lineCap = &#39;round&#39;;
    ctx.lineJoin = &#39;round&#39;;
    ctx.globalCompositeOperation = &#39;lighter&#39;;

    for(var i = 0; i &lt; infall.length; i++){
      var p = infall[i];

      p.px = p.x;
      p.py = p.y;

      var dx = cx - p.x;
      var dy = cy - p.y;
      var d2 = dx * dx + dy * dy;
      var d = Math.sqrt(d2) + 0.001;

      var nx = dx / d;
      var ny = dy / d;

      var tx = -ny * p.spin;
      var ty =  nx * p.spin;

      var pull = (G * M * 420) / Math.max(sr * sr * 0.65, d2);

      var near = smoothstep(sr * 7.5, sr * 1.35, d);
      var swirl = 0.0036 * (0.65 + G * 0.9) * Math.sqrt(M / 6) * near * p.curl;

      var drag = Math.pow(0.992, dt / 16.67);

      p.vx = (p.vx + (nx * pull + tx * swirl) * dt * 0.055) * drag;
      p.vy = (p.vy + (ny * pull + ty * swirl) * dt * 0.055) * drag;

      p.x += p.vx * dt * 0.060;
      p.y += p.vy * dt * 0.060;

      if(
        d &lt; sr * 1.18 ||
        p.x &lt; -120 || p.x &gt; W + 120 ||
        p.y &lt; -120 || p.y &gt; H + 120
      ){
        infall[i] = spawnInfallParticle();
        continue;
      }

      var mvx = p.x - p.px;
      var mvy = p.y - p.py;
      var speed = Math.hypot(mvx, mvy);

      if(speed &lt; 0.001) continue;

      var inv = 1 / speed;
      var dirX = mvx * inv;
      var dirY = mvy * inv;

      var trailLen = clamp(4 + speed * 1.5 + near * 6, 4, 15);

      var curve = (1 - near) * 1.5 + near * (5.5 + speed * 1.2);
      var curveX = -dirY * curve * p.spin;
      var curveY =  dirX * curve * p.spin;

      var alpha = clamp(p.alpha * (0.45 + near * 0.9), 0.12, 0.68);
      var width = clamp(p.size * (0.85 + near * 0.65), 0.55, 1.9);

      ctx.strokeStyle = &#39;hsla(&#39; + p.hue + &#39;,100%,&#39; + (66 + near * 18) + &#39;%,&#39; + (alpha * 0.42) + &#39;)&#39;;
      ctx.lineWidth = width * 1.85;
      ctx.beginPath();
      ctx.moveTo(
        p.px - dirX * trailLen * 0.55,
        p.py - dirY * trailLen * 0.55
      );
      ctx.quadraticCurveTo(
        p.px - dirX * trailLen + curveX * 0.35,
        p.py - dirY * trailLen + curveY * 0.35,
        p.x,
        p.y
      );
      ctx.stroke();

      ctx.strokeStyle = &#39;hsla(&#39; + (p.hue + 6) + &#39;,100%,&#39; + (78 + near * 12) + &#39;%,&#39; + alpha + &#39;)&#39;;
      ctx.lineWidth = width;
      ctx.beginPath();
      ctx.moveTo(
        p.px - dirX * trailLen * 0.35,
        p.py - dirY * trailLen * 0.35
      );
      ctx.quadraticCurveTo(
        p.px - dirX * trailLen * 0.65 + curveX * 0.22,
        p.py - dirY * trailLen * 0.65 + curveY * 0.22,
        p.x,
        p.y
      );
      ctx.stroke();

      ctx.fillStyle = &#39;rgba(255,238,190,&#39; + clamp(alpha * 0.95, 0.16, 0.78) + &#39;)&#39;;
      ctx.beginPath();
      ctx.arc(p.x, p.y, Math.max(0.55, width * 0.72), 0, Math.PI * 2);
      ctx.fill();
    }

    ctx.restore();
  }
  function drawLensingArcs(){
    var r=shadowRadius();
    ctx.save();ctx.lineCap=&#39;round&#39;;
    var pulse=.90+.10*Math.sin(time*.0017);
    ctx.strokeStyle=&#39;rgba(255,126,28,&#39;+(.42*pulse)+&#39;)&#39;;ctx.lineWidth=r*.13;
    ctx.beginPath();ctx.ellipse(cx,cy-r*.10,r*1.62,r*1.48,0,Math.PI*1.08,Math.PI*1.92);ctx.stroke();
    ctx.strokeStyle=&#39;rgba(255,220,145,&#39;+(.24*pulse)+&#39;)&#39;;ctx.lineWidth=r*.070;
    ctx.beginPath();ctx.ellipse(cx,cy+r*.13,r*1.48,r*1.35,0,.10*Math.PI,.90*Math.PI);ctx.stroke();
    ctx.restore();
  }
  function tick(now){
    var dt=last?Math.min(34,now-last):16.67;last=now;time=now;
    var px=pointer.active?(pointer.x-W*.5):0,py=pointer.active?(pointer.y-H*.5):0;
    var tx=baseCx+clamp(px*.018,-W*.014,W*.014),ty=baseCy+clamp(py*.012,-H*.012,H*.012);
    cx+=(tx-cx)*.055;cy+=(ty-cy)*.055;

    background();
    drawDiskGlow();
    drawAccretionRibbon(true);
    drawDiskBands(true);
    drawGasPass(true,false);
    drawGasPass(true,true);
    drawLensingArcs();
    drawShadowAndPhotonRing();
    drawAccretionRibbon(false);
    drawDiskBands(false);
    drawGasPass(false,false);
    drawInfall(dt);
    requestAnimationFrame(tick);
  }
  function pos(e){
    var r=canvas.getBoundingClientRect();pointer.x=e.clientX-r.left;pointer.y=e.clientY-r.top;pointer.active=true;
  }
  canvas.addEventListener(&#39;pointermove&#39;,pos);
  canvas.addEventListener(&#39;pointerenter&#39;,function(e){pos(e);ga(&#39;demo_interaction&#39;,{project:&#39;black-hole-gravity&#39;,kind:&#39;pointer&#39;});},{once:true});
  canvas.addEventListener(&#39;pointerleave&#39;,function(){pointer.active=false;});
  [gravityInput,massInput].forEach(function(el){el.addEventListener(&#39;input&#39;,function(){values();ga(&#39;parameter_change&#39;,{project:&#39;black-hole-gravity&#39;,parameter:el.dataset.control,value:el.value});});});
  particlesInput.addEventListener(&#39;change&#39;,function(){values();buildScene();ga(&#39;parameter_change&#39;,{project:&#39;black-hole-gravity&#39;,parameter:&#39;particles&#39;,value:this.value});});
  resetBtn.addEventListener(&#39;click&#39;,function(){gravityInput.value=.55;massInput.value=6;particlesInput.value=90;values();buildScene();ga(&#39;demo_reset&#39;,{project:&#39;black-hole-gravity&#39;,particles:90});});
  copyBtn.addEventListener(&#39;click&#39;,function(){
    var txt=code.textContent;
    if(navigator.clipboard)navigator.clipboard.writeText(txt).then(function(){var old=copyBtn.textContent;copyBtn.textContent=copyBtn.dataset.copied;setTimeout(function(){copyBtn.textContent=old},1200);ga(&#39;code_copy&#39;,{project:&#39;black-hole-gravity&#39;});});
  });
  window.addEventListener(&#39;resize&#39;,resize);
  resize();values();requestAnimationFrame(tick);
})();
&lt;/script&gt;
&lt;/div&gt;
</description><link>http://juegosenhtml5.blogspot.com/2026/08/agujero-negro-javascript-canvas.html</link><author>noreply@blogger.com (namzug)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-3532846131400833282</guid><pubDate>Tue, 25 Aug 2026 09:21:43 +0000</pubDate><atom:updated>2026-08-25T04:24:13.804-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">canvas</category><category domain="http://www.blogger.com/atom/ns#">HTML5</category><category domain="http://www.blogger.com/atom/ns#">Phaser</category><category domain="http://www.blogger.com/atom/ns#">Phaser 4</category><category domain="http://www.blogger.com/atom/ns#">Responsive</category><category domain="http://www.blogger.com/atom/ns#">Tutoriales</category><title>Canvas responsive en juegos HTML5: guía práctica con Phaser 4</title><description>&lt;p class=&quot;jh-article-lead&quot;&gt;
Hacer un juego responsive no significa simplemente poner &lt;code&gt;width: 100%&lt;/code&gt; al canvas.
En un juego HTML5 hay que coordinar al menos cinco cosas: el tamaño visual del elemento, la resolución
interna del bitmap, la relación de aspecto, las coordenadas del input y la posición del HUD. En esta guía
vamos a resolver cada una con ejemplos prácticos y después veremos cómo aplicar la misma idea en Phaser 4.
&lt;/p&gt;

&lt;div class=&quot;jh-version-badge&quot;&gt;Canvas API · ResizeObserver · DPR · Phaser 4&lt;/div&gt;

&lt;!--IMAGEN 1: reemplaza URL_IMAGEN_PORTADA--&gt;
&lt;figure class=&quot;jh-article-figure&quot;&gt;
  &lt;img alt=&quot;Canvas responsive en juegos HTML5 para móvil tablet y escritorio&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhMOO7p_aIMpeynNrFsvkL00NA7I_mHgO5wlAkb6KtKEozI72SNd2EywTbq74xbWM5gXh4kofyo9Z1-hWMsxNJjAptFQIdeViMzIn5QU77dkjKyc-SMXim_g2I54joZU7I0lp6d_tUpTeZyn491l3QJUz-XT1WUiGdP8htgSrLmZYDnALwfHxDuZmt1jCX_/s1376/Responsive_canvas_in_HTML5_games_202608250414.jpeg&quot; /&gt;
  &lt;figcaption&gt;
    Un juego responsive debe mantener gameplay, nitidez e interfaz utilizables en pantallas con proporciones muy diferentes.
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;div class=&quot;jh-article-note&quot;&gt;
  &lt;div class=&quot;jh-article-note-icon&quot;&gt;R&lt;/div&gt;
  &lt;div&gt;
    &lt;b&gt;Responsive no es igual a estirar&lt;/b&gt;
    &lt;p&gt;
      El objetivo no es deformar un juego de 16:9 hasta llenar cualquier pantalla. Primero decides qué
      permanece fijo, qué puede crecer, qué puede recortarse y qué elementos del HUD deben recolocarse.
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;a name=&#39;more&#39;&gt;&lt;/a&gt;

&lt;h2&gt;El problema: un canvas tiene dos tamaños&lt;/h2&gt;

&lt;p&gt;
Este es el concepto más importante de todo el tutorial. Un elemento &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; tiene:
&lt;/p&gt;

&lt;div class=&quot;jh-compare-grid&quot;&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;Tamaño CSS&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Es el tamaño que ves en la página.&lt;/li&gt;
      &lt;li&gt;Se mide en CSS pixels.&lt;/li&gt;
      &lt;li&gt;Puede ser &lt;code&gt;width: 100%&lt;/code&gt;.&lt;/li&gt;
      &lt;li&gt;No cambia automáticamente la resolución interna.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;Tamaño interno&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Es el bitmap real del canvas.&lt;/li&gt;
      &lt;li&gt;Depende de &lt;code&gt;canvas.width&lt;/code&gt; y &lt;code&gt;canvas.height&lt;/code&gt;.&lt;/li&gt;
      &lt;li&gt;Determina cuántos píxeles renderizamos.&lt;/li&gt;
      &lt;li&gt;Influye directamente en nitidez y rendimiento.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;
Por ejemplo, esto hace que el canvas se vea grande:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;CSS&lt;/b&gt;
    &lt;span&gt;Solo tamaño visual&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;canvas {
  width: 100%;
  height: auto;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Pero si internamente el canvas sigue teniendo una resolución muy pequeña, el navegador deberá escalar
ese bitmap y el resultado puede verse borroso.
&lt;/p&gt;

&lt;h2&gt;1. Empieza con un contenedor, no con el canvas&lt;/h2&gt;

&lt;p&gt;
Una estructura bastante segura para un juego embebido en una web es controlar primero el espacio disponible:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;HTML&lt;/b&gt;
    &lt;span&gt;Contenedor del juego&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;game-shell&quot;&amp;gt;
  &amp;lt;canvas id=&quot;game&quot;&amp;gt;&amp;lt;/canvas&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;CSS&lt;/b&gt;
    &lt;span&gt;Wrapper responsive 16:9&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;html,
body {
  margin: 0;
  background: #071522;
}

#game-shell {
  width: min(100%, 1280px);
  aspect-ratio: 16 / 9;

  margin-inline: auto;

  position: relative;
  overflow: hidden;

  background: #071522;
}

#game {
  display: block;

  width: 100%;
  height: 100%;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Con &lt;code&gt;aspect-ratio: 16 / 9&lt;/code&gt;, el espacio visible conserva la proporción del juego.
En un blog o portal, esta estrategia es especialmente cómoda porque el canvas no intenta ocupar una altura
arbitraria.
&lt;/p&gt;

&lt;h2&gt;2. Haz que el bitmap interno siga al tamaño visible&lt;/h2&gt;

&lt;p&gt;
Ahora vamos a resolver la nitidez. Leeremos el tamaño CSS real del canvas y crearemos un bitmap interno
proporcional a la densidad de píxeles del dispositivo.
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;JavaScript&lt;/b&gt;
    &lt;span&gt;Canvas HiDPI&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;const canvas = document.querySelector(&quot;#game&quot;);
const ctx = canvas.getContext(&quot;2d&quot;);

function resizeCanvas() {
  const rect = canvas.getBoundingClientRect();

  const dpr = Math.min(
    window.devicePixelRatio || 1,
    2
  );

  const width = Math.round(rect.width * dpr);
  const height = Math.round(rect.height * dpr);

  if (
    canvas.width !== width ||
    canvas.height !== height
  ) {
    canvas.width = width;
    canvas.height = height;
  }

  // Trabajaremos en CSS pixels.
  ctx.setTransform(
    dpr, 0,
    0, dpr,
    0, 0
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&quot;jh-article-current&quot;&gt;
  &lt;b&gt;¿Por qué limitar DPR a 2?&lt;/b&gt;
  &lt;p&gt;
    Un teléfono puede tener un &lt;code&gt;devicePixelRatio&lt;/code&gt; de 3 o más. Eso no significa que siempre
    sea buena idea renderizar cuatro, nueve o más veces la cantidad de píxeles. En juegos, limitar el DPR
    es una decisión útil para equilibrar nitidez y fill-rate de GPU.
  &lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;3. Usa ResizeObserver en lugar de depender solo de window.resize&lt;/h2&gt;

&lt;p&gt;
Un canvas puede cambiar de tamaño sin que cambie la ventana: por ejemplo, si se abre un sidebar,
cambia el layout del blog o el juego está dentro de un panel responsive.
&lt;/p&gt;

&lt;p&gt;
Para reaccionar al tamaño real del contenedor:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;JavaScript&lt;/b&gt;
    &lt;span&gt;ResizeObserver&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;const shell = document.querySelector(
  &quot;#game-shell&quot;
);

const resizeObserver =
  new ResizeObserver(() =&amp;gt; {
    resizeCanvas();
  });

resizeObserver.observe(shell);

resizeCanvas();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Así no dependemos de adivinar por qué cambió el layout. Siempre que el contenedor cambie de tamaño,
recalculamos el canvas.
&lt;/p&gt;

&lt;h2&gt;4. No dibujes usando directamente canvas.width&lt;/h2&gt;

&lt;p&gt;
Después de aplicar DPR, &lt;code&gt;canvas.width&lt;/code&gt; ya no representa el tamaño visual. Puede ser el doble
o triple del ancho CSS.
&lt;/p&gt;

&lt;p&gt;
En su lugar utiliza el rect visible:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;JavaScript&lt;/b&gt;
    &lt;span&gt;Tamaño lógico visible&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;const rect =
  canvas.getBoundingClientRect();

const viewWidth = rect.width;
const viewHeight = rect.height;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Ahora podemos dibujar utilizando CSS pixels independientemente del DPR.
&lt;/p&gt;

&lt;h2&gt;5. Ejemplo completo: escena responsive sin motor&lt;/h2&gt;

&lt;p&gt;
Este ejemplo dibuja un pequeño escenario y recoloca los elementos usando el tamaño actual del canvas:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;JavaScript&lt;/b&gt;
    &lt;span&gt;Render responsive&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;function render() {
  const rect =
    canvas.getBoundingClientRect();

  const width = rect.width;
  const height = rect.height;

  ctx.clearRect(
    0,
    0,
    width,
    height
  );

  // Fondo
  ctx.fillStyle = &quot;#071522&quot;;
  ctx.fillRect(
    0,
    0,
    width,
    height
  );

  // Suelo proporcional a la pantalla
  const groundY = height * 0.78;

  ctx.fillStyle = &quot;#173d2c&quot;;
  ctx.fillRect(
    0,
    groundY,
    width,
    height - groundY
  );

  // Jugador
  const playerSize =
    Math.max(
      32,
      Math.min(64, width * 0.06)
    );

  const playerX = width * 0.18;
  const playerY =
    groundY - playerSize;

  ctx.fillStyle = &quot;#22d3ee&quot;;

  ctx.fillRect(
    playerX,
    playerY,
    playerSize,
    playerSize
  );

  // Enemigo
  const enemySize =
    playerSize * 0.8;

  ctx.fillStyle = &quot;#ff8a1f&quot;;

  ctx.fillRect(
    width * 0.72,
    groundY - enemySize,
    enemySize,
    enemySize
  );

  requestAnimationFrame(render);
}

resizeCanvas();
render();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Fíjate en que no existe un &lt;code&gt;x = 900&lt;/code&gt; que dependa de una resolución concreta. Las posiciones
principales se calculan como porcentajes del espacio visible.
&lt;/p&gt;

&lt;h2&gt;6. Pero un juego real necesita coordenadas lógicas&lt;/h2&gt;

&lt;p&gt;
Calcular todo con porcentajes funciona para interfaces simples, pero puede convertirse en un problema
para niveles, físicas y mapas.
&lt;/p&gt;

&lt;p&gt;
En muchos juegos es mejor mantener una resolución lógica fija:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;JavaScript&lt;/b&gt;
    &lt;span&gt;Resolución lógica&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;const GAME_WIDTH = 1280;
const GAME_HEIGHT = 720;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Todo el gameplay vive en ese mundo de 1280 × 720 y el canvas visible solo decide cómo mostrarlo.
&lt;/p&gt;

&lt;p&gt;
Esto produce una separación muy útil:
&lt;/p&gt;

&lt;div class=&quot;jh-article-grid&quot;&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;Game coordinates&lt;/b&gt;
    &lt;span&gt;Posiciones del jugador, enemigos, colisiones, cámara y física.&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;Display coordinates&lt;/b&gt;
    &lt;span&gt;Tamaño real del canvas dentro del navegador.&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;UI coordinates&lt;/b&gt;
    &lt;span&gt;HUD, botones táctiles, safe areas y overlays adaptativos.&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;7. Calcular un escalado FIT manualmente&lt;/h2&gt;

&lt;p&gt;
Supongamos que nuestro juego lógico mide:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Juego&lt;/b&gt;
    &lt;span&gt;Base size&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;1280 × 720&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
y el contenedor disponible tiene cualquier otro tamaño.
&lt;/p&gt;

&lt;p&gt;
Para hacer &lt;b&gt;FIT&lt;/b&gt; sin deformar:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;JavaScript&lt;/b&gt;
    &lt;span&gt;Scale factor&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;const scale = Math.min(
  viewWidth / GAME_WIDTH,
  viewHeight / GAME_HEIGHT
);

const displayWidth =
  GAME_WIDTH * scale;

const displayHeight =
  GAME_HEIGHT * scale;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
El juego completo queda visible. Si la relación de aspecto del dispositivo es distinta, aparecerán bandas
libres en un eje.
&lt;/p&gt;

&lt;h2&gt;FIT vs ENVELOP vs RESIZE&lt;/h2&gt;

&lt;!--IMAGEN 2: reemplaza URL_IMAGEN_ESCALADO--&gt;
&lt;figure class=&quot;jh-article-figure&quot;&gt;
  &lt;img alt=&quot;Comparación de FIT ENVELOP y RESIZE en un canvas responsive&quot; loading=&quot;lazy&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhy14ShczQSMgk3lsDDORSqBf6K5J83bSOpCcW0z8TmwgdCRWhfoLyWr6UCRExz13Ot99R_EVzfM19P4rHzmmi88neKnECKNHMe68lgWpI03TB4KqZTnmz6C1YXmKgrDtlTwzt9EXWOKXnJiXOgkG02iCChQlZznl_UlMwM_BNNgxuKxQQOmGOEJEZmpQhb/s1376/HTML5_game_scaling_modes_diagram_202608250414.jpeg&quot; /&gt;
  &lt;figcaption&gt;
    FIT conserva todo el juego, ENVELOP llena el área recortando extremos y RESIZE cambia realmente el tamaño lógico disponible.
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;div class=&quot;jh-compare-grid&quot;&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;FIT&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Mantiene relación de aspecto.&lt;/li&gt;
      &lt;li&gt;Todo el juego permanece visible.&lt;/li&gt;
      &lt;li&gt;Puede dejar letterbox.&lt;/li&gt;
      &lt;li&gt;Excelente para gameplay con composición fija.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;ENVELOP&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Mantiene relación de aspecto.&lt;/li&gt;
      &lt;li&gt;Llena completamente la pantalla.&lt;/li&gt;
      &lt;li&gt;Puede ocultar los bordes del mundo.&lt;/li&gt;
      &lt;li&gt;Útil si el gameplay tolera crop.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;jh-compare-grid&quot;&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;RESIZE&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Cambia el tamaño real disponible.&lt;/li&gt;
      &lt;li&gt;No obliga a mantener una proporción fija.&lt;/li&gt;
      &lt;li&gt;Ideal para interfaces adaptativas.&lt;/li&gt;
      &lt;li&gt;Necesita relayout del mundo y HUD.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;EXPAND&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Combina ideas de RESIZE y FIT.&lt;/li&gt;
      &lt;li&gt;Utiliza todo el espacio del parent.&lt;/li&gt;
      &lt;li&gt;Puede ser útil para layouts híbridos.&lt;/li&gt;
      &lt;li&gt;Conviene probarlo en dispositivos reales.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;8. Input responsive: transformar pointer a coordenadas del juego&lt;/h2&gt;

&lt;p&gt;
Si el canvas se está escalando, no podemos asumir que:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Incorrecto&lt;/b&gt;
    &lt;span&gt;Pointer&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;const x = event.clientX;
const y = event.clientY;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
sean coordenadas válidas dentro del mundo.
&lt;/p&gt;

&lt;p&gt;
Primero calculamos la posición relativa al canvas:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;JavaScript&lt;/b&gt;
    &lt;span&gt;Pointer local&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;function getPointerPosition(event) {
  const rect =
    canvas.getBoundingClientRect();

  const x =
    event.clientX - rect.left;

  const y =
    event.clientY - rect.top;

  return { x, y };
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Si además utilizas un mundo lógico 1280 × 720 escalado con FIT, debes compensar el factor de escala y
el offset del letterbox:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;JavaScript&lt;/b&gt;
    &lt;span&gt;Display → game coordinates&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;const offsetX =
  (viewWidth - displayWidth) / 2;

const offsetY =
  (viewHeight - displayHeight) / 2;

const gameX =
  (pointerX - offsetX) / scale;

const gameY =
  (pointerY - offsetY) / scale;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&quot;jh-article-note&quot;&gt;
  &lt;div class=&quot;jh-article-note-icon&quot;&gt;!&lt;/div&gt;
  &lt;div&gt;
    &lt;b&gt;Este error rompe muchos juegos móviles&lt;/b&gt;
    &lt;p&gt;
      Visualmente el botón parece estar en un lugar, pero su hit area queda desplazada porque el input
      sigue utilizando coordenadas de pantalla sin convertirlas al sistema lógico.
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;9. Responsive en Phaser 4: el caso sencillo con FIT&lt;/h2&gt;

&lt;p&gt;
Phaser ya incluye un Scale Manager que resuelve gran parte del trabajo anterior.
&lt;/p&gt;

&lt;p&gt;
Si tu juego está diseñado sobre una resolución lógica fija de 1280 × 720:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Phaser 4&lt;/b&gt;
    &lt;span&gt;Scale.FIT&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;import * as Phaser from &quot;phaser&quot;;

const config:
  Phaser.Types.Core.GameConfig = {

  type: Phaser.AUTO,

  width: 1280,
  height: 720,

  parent: &quot;game-container&quot;,

  scale: {
    mode: Phaser.Scale.FIT,

    autoCenter:
      Phaser.Scale.CENTER_BOTH,

    width: 1280,
    height: 720
  },

  scene: [
    MainScene
  ]
};

new Phaser.Game(config);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;code&gt;FIT&lt;/code&gt; mantiene la relación de aspecto y escala el canvas para que entre dentro del parent.
Es una muy buena opción para:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;plataformas;&lt;/li&gt;
  &lt;li&gt;puzzle;&lt;/li&gt;
  &lt;li&gt;arcade;&lt;/li&gt;
  &lt;li&gt;juegos con niveles diseñados sobre una composición fija;&lt;/li&gt;
  &lt;li&gt;juegos donde no quieres mostrar más mundo en dispositivos ultrawide.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;10. CSS del parent para Phaser&lt;/h2&gt;

&lt;p&gt;
El Scale Manager necesita un parent con un tamaño real y predecible:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;CSS&lt;/b&gt;
    &lt;span&gt;Phaser parent&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;html,
body {
  margin: 0;

  width: 100%;
  min-height: 100%;

  background: #071522;
}

#game-container {
  width: 100%;
  height: 100dvh;

  overflow: hidden;

  display: flex;
  align-items: center;
  justify-content: center;
}

#game-container canvas {
  display: block;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Utilizar &lt;code&gt;100dvh&lt;/code&gt; evita parte de los problemas históricos de &lt;code&gt;100vh&lt;/code&gt; en navegadores
móviles donde la barra del navegador entra y sale.
&lt;/p&gt;

&lt;h2&gt;11. ¿Cuándo utilizar RESIZE en Phaser?&lt;/h2&gt;

&lt;p&gt;
Con &lt;code&gt;RESIZE&lt;/code&gt; Phaser ajusta el canvas al espacio disponible en el parent:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Phaser 4&lt;/b&gt;
    &lt;span&gt;Scale.RESIZE&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;scale: {
  mode: Phaser.Scale.RESIZE,

  parent: &quot;game-container&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Aquí la Scene debe reaccionar al nuevo tamaño.
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Phaser 4&lt;/b&gt;
    &lt;span&gt;Resize event&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;create() {
  this.scale.on(
    &quot;resize&quot;,
    this.onResize,
    this
  );

  this.onResize(
    this.scale.gameSize
  );
}

onResize(gameSize) {
  const width =
    gameSize.width;

  const height =
    gameSize.height;

  this.scoreText.setPosition(
    24,
    24
  );

  this.pauseButton.setPosition(
    width - 40,
    40
  );

  this.player.setPosition(
    width * 0.25,
    height * 0.72
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&quot;jh-legacy-box&quot;&gt;
  &lt;b&gt;RESIZE no es gratis&lt;/b&gt;
  &lt;p&gt;
    Un canvas de mayor resolución obliga a la GPU a renderizar más píxeles. En un monitor 4K o un teléfono
    con DPR alto puedes multiplicar enormemente el fill-rate. Añade límites y prueba rendimiento real.
  &lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;12. HUD responsive: usa anchors, no posiciones mágicas&lt;/h2&gt;

&lt;p&gt;
Un error clásico:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Evita&lt;/b&gt;
    &lt;span&gt;Hardcoded HUD&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;pauseButton.setPosition(
  1180,
  40
);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Si el ancho puede cambiar, utiliza el ancho actual:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Mejor&lt;/b&gt;
    &lt;span&gt;Anchor al borde&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;pauseButton.setPosition(
  width - 40,
  40
);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Podemos pensar el HUD como anchors:
&lt;/p&gt;

&lt;div class=&quot;jh-article-grid&quot;&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;Top left&lt;/b&gt;
    &lt;span&gt;Score, monedas, información secundaria.&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;Top right&lt;/b&gt;
    &lt;span&gt;Pausa, settings, botones de sistema.&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;Bottom zones&lt;/b&gt;
    &lt;span&gt;Controles táctiles y acciones principales.&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;13. No escales la UI de móvil como si fuera una miniatura&lt;/h2&gt;

&lt;p&gt;
Imagina que un botón mide 44 px en desktop. Si todo el juego se reduce mucho en un teléfono, ese botón puede
terminar visualmente en 25 px y convertirse en un mal target táctil.
&lt;/p&gt;

&lt;p&gt;
Por eso muchos juegos separan:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;b&gt;world scale:&lt;/b&gt; cámara, escenario, personajes;&lt;/li&gt;
  &lt;li&gt;&lt;b&gt;UI scale:&lt;/b&gt; HUD y botones;&lt;/li&gt;
  &lt;li&gt;&lt;b&gt;touch layout:&lt;/b&gt; zonas específicas para móvil.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
Esto significa que responsive no siempre consiste en escalar exactamente todo con el mismo factor.
&lt;/p&gt;

&lt;h2&gt;14. Portrait y landscape&lt;/h2&gt;

&lt;p&gt;
También debes decidir si tu juego:
&lt;/p&gt;

&lt;div class=&quot;jh-compare-grid&quot;&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;Solo landscape&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Más sencillo de diseñar.&lt;/li&gt;
      &lt;li&gt;Puedes mostrar un aviso al rotar.&lt;/li&gt;
      &lt;li&gt;Útil para platformers y juegos de acción.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;Adaptativo&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Requiere dos composiciones reales.&lt;/li&gt;
      &lt;li&gt;HUD debe relayout.&lt;/li&gt;
      &lt;li&gt;La cámara puede necesitar más o menos mundo visible.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;
No intentes convertir automáticamente un diseño horizontal 16:9 en uno vertical 9:16 si el gameplay no fue
pensado para ello.
&lt;/p&gt;

&lt;h2&gt;15. Safe areas en móviles&lt;/h2&gt;

&lt;p&gt;
Si tu juego utiliza toda la pantalla, evita colocar controles esenciales pegados a los bordes.
En dispositivos con notch o barras del sistema puedes utilizar las CSS environment variables:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;CSS&lt;/b&gt;
    &lt;span&gt;Safe area&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;#game-ui {
  padding-top:
    env(safe-area-inset-top);

  padding-right:
    env(safe-area-inset-right);

  padding-bottom:
    env(safe-area-inset-bottom);

  padding-left:
    env(safe-area-inset-left);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;16. Meta viewport&lt;/h2&gt;

&lt;p&gt;
En una página móvil, asegúrate de tener:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;HTML&lt;/b&gt;
    &lt;span&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;&amp;lt;meta
  name=&quot;viewport&quot;
  content=&quot;width=device-width,
           initial-scale=1,
           viewport-fit=cover&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Sin una configuración correcta del viewport, el navegador móvil puede simular un ancho de layout distinto
y romper todos tus cálculos.
&lt;/p&gt;

&lt;h2&gt;17. Ejemplo Phaser completo con HUD responsive&lt;/h2&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;TypeScript&lt;/b&gt;
    &lt;span&gt;MainScene.ts&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;import * as Phaser from &quot;phaser&quot;;

export class MainScene
  extends Phaser.Scene {

  private scoreText!: Phaser.GameObjects.Text;
  private pauseButton!: Phaser.GameObjects.Text;

  constructor() {
    super(&quot;MainScene&quot;);
  }

  create() {
    this.cameras.main
      .setBackgroundColor(&quot;#071522&quot;);

    this.scoreText =
      this.add.text(
        24,
        24,
        &quot;SCORE: 0&quot;,
        {
          fontFamily: &quot;Arial&quot;,
          fontSize: &quot;24px&quot;,
          color: &quot;#ffffff&quot;
        }
      );

    this.pauseButton =
      this.add.text(
        0,
        0,
        &quot;II&quot;,
        {
          fontFamily: &quot;Arial&quot;,
          fontSize: &quot;28px&quot;,
          color: &quot;#ffffff&quot;
        }
      )
      .setOrigin(0.5)
      .setInteractive();

    this.scale.on(
      &quot;resize&quot;,
      this.layout,
      this
    );

    this.layout(
      this.scale.gameSize
    );
  }

  private layout(
    gameSize:
      Phaser.Structs.Size
  ) {
    const width =
      gameSize.width;

    const height =
      gameSize.height;

    this.scoreText
      .setPosition(
        24,
        24
      );

    this.pauseButton
      .setPosition(
        width - 40,
        40
      );

    // Aquí puedes reposicionar
    // controles o elementos del mundo.

    console.log(
      `viewport: ${width} x ${height}`
    );
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;18. Checklist responsive antes de publicar&lt;/h2&gt;

&lt;ol class=&quot;jh-step-list&quot;&gt;
  &lt;li&gt;
    &lt;div&gt;
      &lt;b&gt;Prueba 16:9&lt;/b&gt;
      Por ejemplo 1920×1080 y 1280×720.
    &lt;/div&gt;
  &lt;/li&gt;

  &lt;li&gt;
    &lt;div&gt;
      &lt;b&gt;Prueba ultrawide&lt;/b&gt;
      Verifica que no revele partes del mapa que deberían estar ocultas.
    &lt;/div&gt;
  &lt;/li&gt;

  &lt;li&gt;
    &lt;div&gt;
      &lt;b&gt;Prueba móvil landscape&lt;/b&gt;
      Comprueba HUD, touch targets y barras del navegador.
    &lt;/div&gt;
  &lt;/li&gt;

  &lt;li&gt;
    &lt;div&gt;
      &lt;b&gt;Prueba portrait&lt;/b&gt;
      Aunque tu juego no lo soporte, la experiencia debe explicar qué hacer.
    &lt;/div&gt;
  &lt;/li&gt;

  &lt;li&gt;
    &lt;div&gt;
      &lt;b&gt;Prueba DPR alto&lt;/b&gt;
      Busca blur, GPU demasiado cargada y consumo de batería.
    &lt;/div&gt;
  &lt;/li&gt;

  &lt;li&gt;
    &lt;div&gt;
      &lt;b&gt;Prueba input&lt;/b&gt;
      Click y touch deben coincidir con lo que el jugador ve.
    &lt;/div&gt;
  &lt;/li&gt;

  &lt;li&gt;
    &lt;div&gt;
      &lt;b&gt;Prueba resize durante gameplay&lt;/b&gt;
      Gira el dispositivo o cambia el tamaño de la ventana.
    &lt;/div&gt;
  &lt;/li&gt;

  &lt;li&gt;
    &lt;div&gt;
      &lt;b&gt;Mide rendimiento&lt;/b&gt;
      No asumas que un canvas más grande es gratis.
    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;Qué estrategia recomiendo&lt;/h2&gt;

&lt;p&gt;
Para la mayoría de juegos 2D del blog empezaría con:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Recomendación&lt;/b&gt;
    &lt;span&gt;Gameplay con resolución fija&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;scale: {
  mode: Phaser.Scale.FIT,
  autoCenter:
    Phaser.Scale.CENTER_BOTH,

  width: 1280,
  height: 720
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
y haría que el HUD tenga reglas específicas para móvil.
&lt;/p&gt;

&lt;p&gt;
Elegiría &lt;code&gt;RESIZE&lt;/code&gt; cuando el diseño realmente deba mostrar más o menos contenido dependiendo
del viewport, por ejemplo:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;estrategia;&lt;/li&gt;
  &lt;li&gt;editores;&lt;/li&gt;
  &lt;li&gt;simuladores;&lt;/li&gt;
  &lt;li&gt;interfaces tipo dashboard;&lt;/li&gt;
  &lt;li&gt;juegos donde revelar más mundo no rompe el gameplay.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Relacionados&lt;/h2&gt;

&lt;div class=&quot;jh-article-links&quot;&gt;
  &lt;a href=&quot;https://juegosenhtml5.blogspot.com/2026/08/phaser-4-vite-typescript-desde-cero.html&quot;&gt;
    Phaser 4 con Vite y TypeScript desde cero
  &lt;/a&gt;

  &lt;a href=&quot;https://juegosenhtml5.blogspot.com/2026/08/migrar-phaser-3-a-phaser-4.html&quot;&gt;
    Cómo migrar de Phaser 3 a Phaser 4
  &lt;/a&gt;
&lt;/div&gt;

&lt;h2&gt;Referencias&lt;/h2&gt;

&lt;div class=&quot;jh-article-links&quot;&gt;
  &lt;a href=&quot;https://docs.phaser.io/phaser/concepts/scale-manager&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;
    Phaser Scale Manager
  &lt;/a&gt;

  &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;
    MDN ResizeObserver
  &lt;/a&gt;

  &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;
    MDN devicePixelRatio
  &lt;/a&gt;

  &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;
    MDN Optimizing Canvas
  &lt;/a&gt;
&lt;/div&gt;

&lt;p class=&quot;jh-article-end&quot;&gt;
Un canvas responsive de calidad mantiene tres cosas alineadas: lo que el jugador ve, las coordenadas donde
vive el juego y el input que recibe. Cuando esas tres capas utilizan la misma estrategia de escalado,
el mismo proyecto puede funcionar correctamente desde un teléfono hasta una pantalla de escritorio.
&lt;/p&gt;</description><link>http://juegosenhtml5.blogspot.com/2026/08/canvas-responsive-juegos-html5.html</link><author>noreply@blogger.com (namzug)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhMOO7p_aIMpeynNrFsvkL00NA7I_mHgO5wlAkb6KtKEozI72SNd2EywTbq74xbWM5gXh4kofyo9Z1-hWMsxNJjAptFQIdeViMzIn5QU77dkjKyc-SMXim_g2I54joZU7I0lp6d_tUpTeZyn491l3QJUz-XT1WUiGdP8htgSrLmZYDnALwfHxDuZmt1jCX_/s72-c/Responsive_canvas_in_HTML5_games_202608250414.jpeg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-3862658953475134488</guid><pubDate>Tue, 25 Aug 2026 09:11:43 +0000</pubDate><atom:updated>2026-08-25T04:12:14.318-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">HTML5</category><category domain="http://www.blogger.com/atom/ns#">Migración</category><category domain="http://www.blogger.com/atom/ns#">Phaser</category><category domain="http://www.blogger.com/atom/ns#">Phaser 3</category><category domain="http://www.blogger.com/atom/ns#">Phaser 4</category><category domain="http://www.blogger.com/atom/ns#">Tutoriales</category><title>Cómo migrar de Phaser 3 a Phaser 4</title><description>&lt;p class=&quot;jh-article-lead&quot;&gt;
Migrar de Phaser 3 a Phaser 4 es bastante menos traumático de lo que parece si tu juego utiliza principalmente
Sprites, Images, Text, Tilemaps, input, física, escenas y tweens. La mayor parte de la API pública se mantuvo
muy parecida. Los cambios importantes aparecen cuando el proyecto toca el renderer, pipelines WebGL, FX,
masks, tint, iluminación o algunas APIs que fueron eliminadas.
&lt;/p&gt;

&lt;div class=&quot;jh-version-badge&quot;&gt;Phaser 3 → Phaser 4.2.1 · Migration Guide 2026&lt;/div&gt;

&lt;!--IMAGEN 1: reemplaza URL_IMAGEN_PORTADA--&gt;
&lt;figure class=&quot;jh-article-figure&quot;&gt;
  &lt;img alt=&quot;Migrar de Phaser 3 a Phaser 4 paso a paso&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgg9qSevJ0KheJbWguH1S4h0rZDkXZYjLY31McCxodYXlNp4yvIhPZjoJXu8mraRt3C4WXBN3uQtIhCk_Mt2pL_TXY4vDmoSYNkJiMOrggGjGpCd-VvbAepTEZBY9PnBbiJHjn9ryzNQuGywCR_QMo_ktXT7uGUL0CaYkFimk639X3Sw1l-_b13cmgGHFLe/s1376/Migrating_game_project_Phaser_tu%E2%80%A6_202608250358.jpeg&quot; /&gt;
  &lt;figcaption&gt;
    Phaser 4 conserva gran parte del flujo de trabajo de Phaser 3, pero cambia profundamente el renderer y los efectos gráficos.
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;div class=&quot;jh-article-note&quot;&gt;
  &lt;div class=&quot;jh-article-note-icon&quot;&gt;✓&lt;/div&gt;
  &lt;div&gt;
    &lt;b&gt;La buena noticia&lt;/b&gt;
    &lt;p&gt;
      Si tu juego usa principalmente objetos estándar de Phaser, la migración suele ser mucho más pequeña
      que una reescritura completa. El trabajo crece sobre todo cuando hay pipelines WebGL, shaders o
      integraciones avanzadas con el renderer.
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;a name=&#39;more&#39;&gt;&lt;/a&gt;

&lt;h2&gt;¿Vale la pena migrar a Phaser 4?&lt;/h2&gt;

&lt;p&gt;
Phaser 4 no es simplemente una actualización de versión. El renderer WebGL fue reconstruido y la arquitectura
interna cambió bastante, pero el equipo de Phaser intentó mantener la API que utiliza la mayoría de desarrolladores
lo más familiar posible.
&lt;/p&gt;

&lt;p&gt;Esto significa que muchos sistemas habituales continúan funcionando con pocos cambios:&lt;/p&gt;

&lt;div class=&quot;jh-article-grid&quot;&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;Normalmente cambia poco&lt;/b&gt;
    &lt;span&gt;Sprites, Images, Text, Groups, Scene management, input, audio, tweens y física Arcade/Matter.&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;Requiere revisión&lt;/b&gt;
    &lt;span&gt;FX, masks, tint, iluminación, DynamicTexture, algunas constantes y estructuras auxiliares.&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;Requiere más trabajo&lt;/b&gt;
    &lt;span&gt;Custom WebGL pipelines, shaders avanzados y código que accede directamente al renderer.&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;jh-article-current&quot;&gt;
  &lt;b&gt;Antes de tocar código&lt;/b&gt;
  &lt;p&gt;
    No hagas la migración directamente sobre tu única copia del proyecto. Crea una rama o un backup y
    asegúrate de que la versión Phaser 3 funciona correctamente antes de empezar.
  &lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;1. Crea una rama exclusiva para la migración&lt;/h2&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Git&lt;/b&gt;&lt;span&gt;Rama de migración&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;git checkout -b migrate/phaser-4&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Antes de continuar, ejecuta el juego en Phaser 3 y revisa al menos:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;inicio y carga de assets;&lt;/li&gt;
  &lt;li&gt;cambio entre escenas;&lt;/li&gt;
  &lt;li&gt;input;&lt;/li&gt;
  &lt;li&gt;física y colisiones;&lt;/li&gt;
  &lt;li&gt;audio;&lt;/li&gt;
  &lt;li&gt;efectos visuales;&lt;/li&gt;
  &lt;li&gt;guardado;&lt;/li&gt;
  &lt;li&gt;game over y reinicio.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;2. Haz un inventario de las partes delicadas&lt;/h2&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Búsquedas útiles&lt;/b&gt;&lt;span&gt;VS Code / IDE&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;setPipeline
PostFX
preFX
postFX
BitmapMask
setTintFill
Phaser.Geom.Point
Phaser.Struct.Set
Phaser.Struct.Map
Math.TAU
DynamicTexture&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Si ninguna de esas piezas aparece y el proyecto usa objetos normales, probablemente estás ante una migración
relativamente sencilla.
&lt;/p&gt;

&lt;h2&gt;3. Actualiza Phaser&lt;/h2&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;npm&lt;/b&gt;&lt;span&gt;Actualizar dependencia&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;npm install phaser@4.2.1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Después inicia el proyecto sin intentar corregir nada por adelantado:&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Terminal&lt;/b&gt;&lt;span&gt;Ejecutar&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;npm run dev&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Los errores que aparezcan ahora se convierten en tu lista real de migración. Es mejor trabajar desde errores
concretos que modificar cientos de líneas “por si acaso”.
&lt;/p&gt;

&lt;h2&gt;4. Corrige el import de Phaser si utilizas NPM&lt;/h2&gt;

&lt;h3&gt;Phaser 3&lt;/h3&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Antes&lt;/b&gt;&lt;span&gt;Import habitual&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;import Phaser from &quot;phaser&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;Phaser 4&lt;/h3&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Después&lt;/b&gt;&lt;span&gt;Wildcard import&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;import * as Phaser from &quot;phaser&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Si cargas Phaser como variable global mediante CDN, este cambio concreto de import no aplica.&lt;/p&gt;

&lt;h2&gt;5. El renderer cambió por completo&lt;/h2&gt;

&lt;p&gt;
Phaser 3 utilizaba un sistema basado en &lt;b&gt;WebGL Pipelines&lt;/b&gt;. Phaser 4 lo reemplaza por una
arquitectura basada en &lt;b&gt;RenderNodes&lt;/b&gt;.
&lt;/p&gt;

&lt;div class=&quot;jh-compare-grid&quot;&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;Phaser 3&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Pipelines WebGL personalizados.&lt;/li&gt;
      &lt;li&gt;Más responsabilidad sobre el estado WebGL.&lt;/li&gt;
      &lt;li&gt;Shaders y render custom muy ligados al pipeline.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;Phaser 4&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Arquitectura mediante RenderNodes.&lt;/li&gt;
      &lt;li&gt;Responsabilidades de render más separadas.&lt;/li&gt;
      &lt;li&gt;Modelo más predecible para extensiones gráficas.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;
Si tu juego solo utiliza Sprite, Image, Text, Tilemap y otros objetos estándar, normalmente no necesitas
reescribir nada por este cambio.
&lt;/p&gt;

&lt;div class=&quot;jh-legacy-box&quot;&gt;
  &lt;b&gt;Proyecto con custom pipeline&lt;/b&gt;
  &lt;p&gt;
    Si escribiste un Pipeline WebGL propio en Phaser 3, trátalo como una subtarea independiente. Esa parte
    debe migrarse al nuevo sistema de render nodes / filtros, no limitarse a un cambio de nombres.
  &lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;6. FX y Masks ahora usan el nuevo sistema de Filters&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;b&gt;BitmapMask:&lt;/b&gt; fue eliminado y su funcionalidad pasa al nuevo Mask Filter.&lt;/li&gt;
  &lt;li&gt;&lt;b&gt;Bloom:&lt;/b&gt; ahora puede crearse mediante &lt;code&gt;Phaser.Actions.AddEffectBloom()&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;b&gt;Shine:&lt;/b&gt; ahora utiliza &lt;code&gt;Phaser.Actions.AddEffectShine()&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;b&gt;Circle:&lt;/b&gt; puede migrarse mediante &lt;code&gt;Phaser.Actions.AddMaskShape()&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;b&gt;Gradient:&lt;/b&gt; pasó a ser un Game Object propio.&lt;/li&gt;
  &lt;li&gt;&lt;b&gt;preFX / postFX:&lt;/b&gt; el modelo anterior fue sustituido por el nuevo sistema de filtros.&lt;/li&gt;
&lt;/ul&gt;

&lt;!--IMAGEN 2: reemplaza URL_IMAGEN_MIGRACION--&gt;
&lt;figure class=&quot;jh-article-figure&quot;&gt;
  &lt;img alt=&quot;Comparación de cambios al migrar de Phaser 3 a Phaser 4&quot; loading=&quot;lazy&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4B34w5mrqyhk4Pk5wYkxUJQi4BX2bKqQN8q0WptJkK4YReVc7P4NykzEXqlhHEsi_Dny9WVerAPLMEpSRJns7-EfxYVYecSFcZbsoWuoXRcvgv0CFJc3J-nrPsgnk_tZpnEGKzckLkd1xutskHidF8abZTeWTkPjHZIoG1ezupR9EK426TqU8lQXda7Bp/s1376/Phaser_3_versus_Phaser_4_202608250358.jpeg&quot; /&gt;
  &lt;figcaption&gt;
    La mayor parte del gameplay continúa igual; renderer, pipelines, FX y algunas APIs auxiliares concentran la migración.
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2&gt;7. setTintFill() cambió&lt;/h2&gt;

&lt;h3&gt;Phaser 3&lt;/h3&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Antes&lt;/b&gt;&lt;span&gt;Fill tint&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;player.setTintFill(0xff0000);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;Phaser 4&lt;/h3&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Después&lt;/b&gt;&lt;span&gt;Tint + modo&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;player
  .setTint(0xff0000)
  .setTintMode(Phaser.TintModes.FILL);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Phaser 4 separa el color del modo y ofrece modos como MULTIPLY, FILL, ADD, SCREEN, OVERLAY y HARD_LIGHT.
&lt;/p&gt;

&lt;h2&gt;8. La iluminación se activa de forma más directa&lt;/h2&gt;

&lt;h3&gt;Phaser 3&lt;/h3&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Antes&lt;/b&gt;&lt;span&gt;Lighting pipeline&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;player.setPipeline(&quot;Light2D&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;Phaser 4&lt;/h3&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Después&lt;/b&gt;&lt;span&gt;Lighting component&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;player.setLighting(true);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Si utilizas normal maps o iluminación personalizada, revisa esa escena completa y no solo una línea.
&lt;/p&gt;

&lt;h2&gt;9. Phaser.Geom.Point desapareció&lt;/h2&gt;

&lt;h3&gt;Phaser 3&lt;/h3&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Antes&lt;/b&gt;&lt;span&gt;Point&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;const target = new Phaser.Geom.Point(
  400,
  300
);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;Phaser 4&lt;/h3&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Después&lt;/b&gt;&lt;span&gt;Vector2&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;const target = new Phaser.Math.Vector2(
  400,
  300
);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;10. Math.TAU ahora tiene su valor matemático correcto&lt;/h2&gt;

&lt;p&gt;
En Phaser 3, &lt;code&gt;Math.TAU&lt;/code&gt; tenía un valor equivalente a &lt;code&gt;PI / 2&lt;/code&gt;. En Phaser 4 representa
correctamente una vuelta completa:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Phaser 4&lt;/b&gt;&lt;span&gt;Constante&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;TAU = Math.PI * 2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Si tu código antiguo lo utilizaba esperando PI / 2, cambia a:&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Phaser 4&lt;/b&gt;&lt;span&gt;Cuarto de vuelta&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;Phaser.Math.PI_OVER_2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;11. Phaser.Struct.Set y Phaser.Struct.Map → JavaScript nativo&lt;/h2&gt;

&lt;h3&gt;Antes&lt;/h3&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Phaser 3&lt;/b&gt;&lt;span&gt;Struct&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;const enemies = new Phaser.Struct.Set();
const items = new Phaser.Struct.Map();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;Después&lt;/h3&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Phaser 4&lt;/b&gt;&lt;span&gt;JavaScript nativo&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;const enemies = new Set();
const items = new Map();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Si utilizabas métodos específicos de las estructuras de Phaser, revisa sus equivalentes en Set y Map nativos.
&lt;/p&gt;

&lt;h2&gt;12. DynamicTexture requiere render()&lt;/h2&gt;

&lt;p&gt;
Si una textura dinámica aparece vacía después de migrar, revisa si falta ejecutar:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Phaser 4&lt;/b&gt;&lt;span&gt;DynamicTexture&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;dynamicTexture.render();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;13. Cámara: lo normal sigue funcionando&lt;/h2&gt;

&lt;p&gt;
Scroll, zoom, rotation y follow suelen continuar sin cambios importantes. Si accedes directamente a matrices
internas de cámara, sí debes revisar la implementación.
&lt;/p&gt;

&lt;h2&gt;14. APIs que fueron eliminadas&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;antiguos Game Objects &lt;b&gt;Mesh&lt;/b&gt; y &lt;b&gt;Plane&lt;/b&gt;;&lt;/li&gt;
  &lt;li&gt;plugins &lt;b&gt;Camera3D&lt;/b&gt; y &lt;b&gt;Layer3D&lt;/b&gt;;&lt;/li&gt;
  &lt;li&gt;soporte antiguo para IE9;&lt;/li&gt;
  &lt;li&gt;plugins Spine incluidos antiguamente en Phaser ya no son el camino recomendado.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;jh-article-note&quot;&gt;
  &lt;div class=&quot;jh-article-note-icon&quot;&gt;i&lt;/div&gt;
  &lt;div&gt;
    &lt;b&gt;Mesh2D no es el viejo Mesh&lt;/b&gt;
    &lt;p&gt;
      Phaser 4.2 introdujo un nuevo Game Object Mesh2D, pero no debes asumir que sea un reemplazo compatible
      línea por línea con el Mesh eliminado de Phaser 3.
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;15. roundPixels cambió de valor por defecto&lt;/h2&gt;

&lt;p&gt;
En Phaser 4 &lt;code&gt;roundPixels&lt;/code&gt; es &lt;code&gt;false&lt;/code&gt; por defecto. Si tu pixel art dependía de esa
opción, haz la intención explícita:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;b&gt;Config&lt;/b&gt;&lt;span&gt;Si lo necesitas&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;render: {
  roundPixels: true
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;16. Cuidado con compressed textures&lt;/h2&gt;

&lt;p&gt;
Phaser 4 cambió la orientación interna de texturas para alinearse mejor con WebGL. Si utilizas texturas
comprimidas, vuelve a generarlas para la orientación usada por Phaser 4.
&lt;/p&gt;

&lt;h2&gt;17. Checklist de migración&lt;/h2&gt;

&lt;ol class=&quot;jh-step-list&quot;&gt;
  &lt;li&gt;&lt;div&gt;&lt;b&gt;Backup / branch&lt;/b&gt;Parte de una versión Phaser 3 que sabes que funciona.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;b&gt;Actualiza la dependencia&lt;/b&gt;Instala Phaser 4 y ejecuta antes de cambiar APIs al azar.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;b&gt;Corrige imports&lt;/b&gt;Si utilizas NPM, cambia default import por wildcard import.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;b&gt;Revisa renderer avanzado&lt;/b&gt;Pipelines y shaders personalizados son la parte de mayor riesgo.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;b&gt;Migra FX, masks y tint&lt;/b&gt;Son cambios localizados pero muy visibles.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;b&gt;Revisa APIs pequeñas&lt;/b&gt;Vector2, TAU, Set, Map y DynamicTexture.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;b&gt;Busca elementos eliminados&lt;/b&gt;Hazlo antes de invertir horas en la migración.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;b&gt;Prueba el juego completo&lt;/b&gt;No basta con que compile y muestre la primera Scene.&lt;/div&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;18. Qué probar después de que compile&lt;/h2&gt;

&lt;div class=&quot;jh-compare-grid&quot;&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;Gameplay&lt;/h3&gt;
    &lt;ul&gt;&lt;li&gt;Input.&lt;/li&gt;&lt;li&gt;Física.&lt;/li&gt;&lt;li&gt;Colisiones.&lt;/li&gt;&lt;li&gt;Tweens.&lt;/li&gt;&lt;li&gt;Scenes.&lt;/li&gt;&lt;/ul&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;Render&lt;/h3&gt;
    &lt;ul&gt;&lt;li&gt;FX.&lt;/li&gt;&lt;li&gt;Masks.&lt;/li&gt;&lt;li&gt;Tint.&lt;/li&gt;&lt;li&gt;Luces.&lt;/li&gt;&lt;li&gt;Texturas comprimidas.&lt;/li&gt;&lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;jh-compare-grid&quot;&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;Plataforma&lt;/h3&gt;
    &lt;ul&gt;&lt;li&gt;Chrome / Chromium.&lt;/li&gt;&lt;li&gt;Firefox.&lt;/li&gt;&lt;li&gt;Móvil real.&lt;/li&gt;&lt;li&gt;Resize.&lt;/li&gt;&lt;li&gt;Pérdida de foco.&lt;/li&gt;&lt;/ul&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;Performance&lt;/h3&gt;
    &lt;ul&gt;&lt;li&gt;FPS.&lt;/li&gt;&lt;li&gt;Frame time.&lt;/li&gt;&lt;li&gt;Memoria.&lt;/li&gt;&lt;li&gt;Carga inicial.&lt;/li&gt;&lt;li&gt;Errores de consola.&lt;/li&gt;&lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;¿Cuánto trabajo puede tomar?&lt;/h2&gt;

&lt;div class=&quot;jh-article-grid&quot;&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;Riesgo bajo&lt;/b&gt;
    &lt;span&gt;Sprites, Text, Tilemaps, Arcade Physics, input, tweens y Scenes estándar.&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;Riesgo medio&lt;/b&gt;
    &lt;span&gt;FX, masks, tint, DynamicTexture, iluminación y utilidades antiguas.&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;Riesgo alto&lt;/b&gt;
    &lt;span&gt;Pipelines WebGL, shaders custom, plugins gráficos propios o APIs eliminadas.&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;
La estrategia más práctica es actualizar, ejecutar y dejar que los errores reales te muestren la lista de trabajo.
&lt;/p&gt;

&lt;h2&gt;Si empiezas un proyecto nuevo&lt;/h2&gt;

&lt;p&gt;
Si no estás migrando un juego existente, arranca directamente con Phaser 4 y un entorno moderno.
&lt;/p&gt;

&lt;div class=&quot;jh-article-links&quot;&gt;
  &lt;a href=&quot;https://juegosenhtml5.blogspot.com/2026/08/phaser-4-vite-typescript-desde-cero.html&quot;&gt;
    Phaser 4 con Vite y TypeScript desde cero
  &lt;/a&gt;
&lt;/div&gt;

&lt;h2&gt;Referencias oficiales&lt;/h2&gt;

&lt;div class=&quot;jh-article-links&quot;&gt;
  &lt;a href=&quot;https://phaser.io/news/2026/04/migrating-from-phaser-3-to-phaser-4-what-you-need-to-know&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Guía oficial de migración&lt;/a&gt;
  &lt;a href=&quot;https://phaser.io/news/2026/05/phaser-3-vs-phaser-4&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Phaser 3 vs Phaser 4&lt;/a&gt;
  &lt;a href=&quot;https://phaser.io/phaser4&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Phaser 4&lt;/a&gt;
&lt;/div&gt;

&lt;p class=&quot;jh-article-end&quot;&gt;
La clave es no tratar Phaser 4 como un framework completamente diferente. Migra primero lo que realmente
rompió, prueba por bloques y deja renderer avanzado, shaders y efectos para una fase controlada.
&lt;/p&gt;</description><link>http://juegosenhtml5.blogspot.com/2026/08/migrar-phaser-3-a-phaser-4.html</link><author>noreply@blogger.com (namzug)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgg9qSevJ0KheJbWguH1S4h0rZDkXZYjLY31McCxodYXlNp4yvIhPZjoJXu8mraRt3C4WXBN3uQtIhCk_Mt2pL_TXY4vDmoSYNkJiMOrggGjGpCd-VvbAepTEZBY9PnBbiJHjn9ryzNQuGywCR_QMo_ktXT7uGUL0CaYkFimk639X3Sw1l-_b13cmgGHFLe/s72-c/Migrating_game_project_Phaser_tu%E2%80%A6_202608250358.jpeg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-1817891556512379540</guid><pubDate>Mon, 24 Aug 2026 21:18:15 +0000</pubDate><atom:updated>2026-08-25T06:34:20.087-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">HTML5</category><category domain="http://www.blogger.com/atom/ns#">Phaser</category><category domain="http://www.blogger.com/atom/ns#">Phaser 4</category><category domain="http://www.blogger.com/atom/ns#">Tutoriales</category><category domain="http://www.blogger.com/atom/ns#">TypeScript</category><category domain="http://www.blogger.com/atom/ns#">Vite</category><title>Phaser 4 con Vite y TypeScript desde cero: guía completa</title><description>&lt;p class=&quot;jh-article-lead&quot;&gt;
Phaser 4 ya es una versión estable del framework y, si vas a comenzar un proyecto nuevo en 2026, tiene sentido combinarlo con un entorno moderno de desarrollo. En este tutorial vamos a crear una base con &lt;b&gt;Phaser 4, Vite y TypeScript&lt;/b&gt;, entender qué hace cada herramienta y dejar una estructura preparada para crecer sin convertir el proyecto en un único archivo enorme.
&lt;/p&gt;

&lt;div class=&quot;jh-version-badge&quot;&gt;Phaser 4.2.1 · Vite · TypeScript&lt;/div&gt;

&lt;figure class=&quot;jh-article-figure&quot;&gt;
  &lt;img alt=&quot;Phaser 4 con Vite y TypeScript desde cero&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiGMCMEkCUU49BxM0C9Idj8xgKG_JwPP90rMrOGdMDttsmQAF9rg4Ke9V7wlg7QOCnQIu7b1NWKL4v-USlRiZ1rt84iJETdvrdHKmvPrIuxVFBstFmX5IleRnQp28Z-YTRJpuighcVUgbH_IpRRT3YhWE9JP040lK28CwsPabOmuIVQmwEVeVe2AU7EtR2C/s1376/Creating_tutorial_cover_image_202608241611.jpeg&quot; /&gt;
  &lt;figcaption&gt;
    Proyecto moderno con Phaser 4, TypeScript, Vite y recarga rápida durante el desarrollo.
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;div class=&quot;jh-article-note&quot;&gt;
  &lt;div class=&quot;jh-article-note-icon&quot;&gt;★&lt;/div&gt;
  &lt;div&gt;
    &lt;b&gt;Qué vamos a conseguir&lt;/b&gt;
    &lt;p&gt;
      Al final tendrás un proyecto Phaser ejecutándose con servidor de desarrollo, TypeScript,
      recarga rápida con Vite y una separación inicial entre escenas, sistemas, assets e interfaz.
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;a name=&#39;more&#39;&gt;&lt;/a&gt;

&lt;h2&gt;Por qué Phaser 4 + Vite + TypeScript&lt;/h2&gt;

&lt;p&gt;
Phaser sigue siendo una de las opciones más directas para crear juegos 2D que corren en el navegador.
Phaser 4 conserva gran parte de la filosofía y de la API pública de Phaser 3, pero su renderer fue
reescrito y la rama 4 incorpora sistemas gráficos más modernos.
&lt;/p&gt;

&lt;p&gt;
Para un proyecto nuevo utilizaremos tres piezas:
&lt;/p&gt;

&lt;div class=&quot;jh-article-grid&quot;&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;Phaser 4&lt;/b&gt;
    &lt;span&gt;Motor 2D: escenas, input, sprites, cámaras, audio, animaciones, física y render.&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;TypeScript&lt;/b&gt;
    &lt;span&gt;Tipado estático, autocompletado y errores detectados antes de ejecutar el juego.&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;b&gt;Vite&lt;/b&gt;
    &lt;span&gt;Servidor de desarrollo, módulos ES, HMR y generación del build de producción.&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;
La idea importante es que &lt;b&gt;Vite no sustituye a Phaser&lt;/b&gt;. Vite se ocupa del entorno de
desarrollo y del empaquetado; Phaser sigue siendo el runtime del juego.
&lt;/p&gt;

&lt;h2&gt;1. Crear el proyecto con la herramienta oficial&lt;/h2&gt;

&lt;p&gt;
Phaser dispone de una herramienta oficial para crear proyectos. Desde una terminal ejecuta:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Terminal&lt;/b&gt;
    &lt;span&gt;Crear proyecto&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;npm create @phaserjs/game@latest&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
El asistente te preguntará qué tipo de proyecto deseas crear. Para esta guía selecciona:
&lt;/p&gt;

&lt;ol class=&quot;jh-step-list&quot;&gt;
  &lt;li&gt;
    &lt;div&gt;
      &lt;b&gt;Web Bundler&lt;/b&gt;
      No necesitamos React, Vue ni otro framework para este ejemplo.
    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;div&gt;
      &lt;b&gt;Vite&lt;/b&gt;
      Será nuestro servidor de desarrollo y bundler.
    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;div&gt;
      &lt;b&gt;TypeScript&lt;/b&gt;
      Utilizaremos archivos &lt;code&gt;.ts&lt;/code&gt; desde el inicio.
    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;div&gt;
      &lt;b&gt;Nombre del proyecto&lt;/b&gt;
      Por ejemplo: &lt;code&gt;phaser4-starter&lt;/code&gt;.
    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;
También puedes indicar directamente la carpeta:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Terminal&lt;/b&gt;
    &lt;span&gt;Nombre de carpeta&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;npm create @phaserjs/game@latest phaser4-starter&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;2. Ejecutar el servidor de desarrollo&lt;/h2&gt;

&lt;p&gt;
Entra en la carpeta del proyecto y ejecuta el script de desarrollo que incluye la plantilla:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Terminal&lt;/b&gt;
    &lt;span&gt;Development server&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;cd phaser4-starter
npm install
npm run dev&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Vite levantará un servidor local. La dirección suele parecerse a:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Vite&lt;/b&gt;
    &lt;span&gt;Servidor local&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;Local: http://localhost:5173/&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
No abras el &lt;code&gt;index.html&lt;/code&gt; directamente con doble clic. Utilizar un servidor local evita
problemas con módulos, carga de recursos y políticas del navegador.
&lt;/p&gt;

&lt;div class=&quot;jh-article-current&quot;&gt;
  &lt;b&gt;Ventaja de Vite&lt;/b&gt;
  &lt;p&gt;
    Cuando modificas TypeScript, CSS o partes del proyecto, Vite reconstruye rápidamente el módulo
    afectado. Esto reduce mucho el ciclo editar → guardar → probar.
  &lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;3. Una estructura de carpetas que pueda crecer&lt;/h2&gt;

&lt;p&gt;
Para un experimento de veinte líneas cualquier estructura funciona. El problema llega cuando aparecen
menús, enemigos, audio, guardado, efectos y varias escenas.
&lt;/p&gt;

&lt;p&gt;
Una estructura inicial razonable puede ser:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Proyecto&lt;/b&gt;
    &lt;span&gt;Arquitectura sugerida&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;phaser4-starter/
│
├─ public/
│
├─ src/
│  ├─ assets/
│  │  ├─ images/
│  │  └─ audio/
│  │
│  ├─ scenes/
│  │  ├─ BootScene.ts
│  │  └─ MainScene.ts
│  │
│  ├─ systems/
│  │  └─ InputSystem.ts
│  │
│  ├─ ui/
│  │  └─ HUD.ts
│  │
│  └─ main.ts
│
├─ index.html
├─ package.json
├─ tsconfig.json
└─ vite.config.ts&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;figure class=&quot;jh-article-figure&quot;&gt;
  &lt;img alt=&quot;Arquitectura y estructura de carpetas para Phaser 4 con Vite y TypeScript&quot; loading=&quot;lazy&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhAexAwg5pMLyVpXMPEhX12G6CxkKZgkpG1hCY9z-Mb37yatHTLbF5RIjPSw6jEUVE6eam1v4WBYuxjLOJwEcuZmPen8Hxs0ilqihfESkpyDv8HexaOZ3t4MkN898XZmvddx1rad2m55zAlemWZxC9u_AUMQbOBFXrNMVXcP6GIiPrv2x6ruEKM_Xbq_QX/s1376/Creating_project_architecture_mo%E2%80%A6_202608241611.jpeg&quot; /&gt;
  &lt;figcaption&gt;
    Una estructura sencilla separa escenas, assets, sistemas e interfaz sin complicar demasiado el proyecto.
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;
No es una regla obligatoria. La arquitectura debe crecer con el juego. Lo importante es evitar que
&lt;code&gt;MainScene.ts&lt;/code&gt; termine controlando absolutamente todo.
&lt;/p&gt;

&lt;h2&gt;4. Crear nuestra primera Scene&lt;/h2&gt;

&lt;p&gt;
Vamos a crear una escena completamente procedural. No necesitaremos imágenes externas: Phaser dibujará
los elementos con objetos básicos. Esto permite validar que la instalación funciona antes de añadir assets.
&lt;/p&gt;

&lt;p&gt;Crea:&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Archivo&lt;/b&gt;
    &lt;span&gt;src/scenes/MainScene.ts&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;import Phaser from &quot;phaser&quot;;

export class MainScene extends Phaser.Scene {
  constructor() {
    super(&quot;MainScene&quot;);
  }

  create() {
    this.cameras.main.setBackgroundColor(&quot;#071522&quot;);

    this.add
      .text(32, 28, &quot;Phaser 4 + TypeScript&quot;, {
        fontFamily: &quot;Arial&quot;,
        fontSize: &quot;28px&quot;,
        color: &quot;#ffffff&quot;
      });

    // Jugador
    this.add.rectangle(
      190,
      300,
      48,
      64,
      0x22d3ee
    );

    // Enemigos
    const enemyPositions = [
      [470, 150],
      [600, 220],
      [520, 360],
      [700, 285]
    ];

    enemyPositions.forEach(([x, y]) =&amp;gt; {
      this.add.rectangle(
        x,
        y,
        42,
        42,
        0xff8a1f
      );
    });

    this.add
      .text(32, 420, &quot;El proyecto está funcionando.&quot;, {
        fontFamily: &quot;Arial&quot;,
        fontSize: &quot;20px&quot;,
        color: &quot;#94a3b8&quot;
      });
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;5. Crear la configuración principal&lt;/h2&gt;

&lt;p&gt;
Ahora abre &lt;code&gt;src/main.ts&lt;/code&gt;. La configuración indica a Phaser cómo crear el juego y qué escena
debe arrancar primero.
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;TypeScript&lt;/b&gt;
    &lt;span&gt;src/main.ts&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;import Phaser from &quot;phaser&quot;;
import { MainScene } from &quot;./scenes/MainScene&quot;;

const config: Phaser.Types.Core.GameConfig = {
  type: Phaser.AUTO,

  width: 960,
  height: 540,

  backgroundColor: &quot;#071522&quot;,

  parent: &quot;game-container&quot;,

  scene: [
    MainScene
  ]
};

new Phaser.Game(config);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;code&gt;Phaser.AUTO&lt;/code&gt; permite que Phaser seleccione el renderer apropiado. El tamaño
&lt;code&gt;960 × 540&lt;/code&gt; es nuestra resolución lógica inicial; más adelante veremos cómo convertirla en
un layout responsive.
&lt;/p&gt;

&lt;h2&gt;6. El contenedor HTML&lt;/h2&gt;

&lt;p&gt;
El valor:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Config&lt;/b&gt;
    &lt;span&gt;parent&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;parent: &quot;game-container&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
indica que Phaser insertará su canvas dentro de un elemento HTML con ese identificador.
&lt;/p&gt;

&lt;p&gt;
En &lt;code&gt;index.html&lt;/code&gt; asegúrate de tener:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;HTML&lt;/b&gt;
    &lt;span&gt;index.html&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;game-container&quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;7. ¿Dónde entran preload, create y update?&lt;/h2&gt;

&lt;p&gt;
El ciclo clásico de una Scene continúa siendo uno de los conceptos fundamentales de Phaser:
&lt;/p&gt;

&lt;div class=&quot;jh-compare-grid&quot;&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;preload()&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Carga imágenes.&lt;/li&gt;
      &lt;li&gt;Carga audio.&lt;/li&gt;
      &lt;li&gt;Carga atlas y JSON.&lt;/li&gt;
      &lt;li&gt;Se ejecuta antes de crear la escena.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;create()&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Crea objetos.&lt;/li&gt;
      &lt;li&gt;Configura input.&lt;/li&gt;
      &lt;li&gt;Construye el mundo.&lt;/li&gt;
      &lt;li&gt;Se ejecuta una vez.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;jh-compare-grid&quot;&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;update()&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Se ejecuta durante el game loop.&lt;/li&gt;
      &lt;li&gt;Actualiza gameplay.&lt;/li&gt;
      &lt;li&gt;Lee input continuo.&lt;/li&gt;
      &lt;li&gt;No debe llenarse de trabajo innecesario.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;

  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;Systems&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Extraen lógica reutilizable.&lt;/li&gt;
      &lt;li&gt;Reducen escenas gigantes.&lt;/li&gt;
      &lt;li&gt;Facilitan test y mantenimiento.&lt;/li&gt;
      &lt;li&gt;No hace falta crearlos demasiado pronto.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;8. Añadir assets después de validar el proyecto&lt;/h2&gt;

&lt;p&gt;
Una vez que el canvas aparece y el servidor funciona, entonces sí conviene comenzar a cargar sprites.
Por ejemplo:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;TypeScript&lt;/b&gt;
    &lt;span&gt;preload()&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;preload() {
  this.load.image(
    &quot;player&quot;,
    &quot;assets/images/player.png&quot;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Y dentro de &lt;code&gt;create()&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;TypeScript&lt;/b&gt;
    &lt;span&gt;create()&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;create() {
  this.add.image(
    480,
    270,
    &quot;player&quot;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Separar primero el problema de instalación del problema de los assets hace que depurar sea mucho más fácil.
Si el canvas procedural funciona y una imagen no aparece después, ya sabes que el problema está en la ruta
o en la carga del asset, no en Phaser o Vite.
&lt;/p&gt;

&lt;h2&gt;9. Crear el build para producción&lt;/h2&gt;

&lt;p&gt;
Durante el desarrollo utilizamos:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Terminal&lt;/b&gt;
    &lt;span&gt;Desarrollo&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;npm run dev&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Cuando quieras publicar el juego:
&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;
    &lt;b&gt;Terminal&lt;/b&gt;
    &lt;span&gt;Producción&lt;/span&gt;
  &lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Vite generará la versión optimizada para producción, normalmente dentro de &lt;code&gt;dist/&lt;/code&gt;.
Esa es la carpeta que después podrás desplegar en un servidor, GitHub Pages, itch.io o un portal compatible
con juegos HTML5.
&lt;/p&gt;

&lt;h2&gt;10. Errores comunes al empezar&lt;/h2&gt;

&lt;div class=&quot;jh-legacy-box&quot;&gt;
  &lt;b&gt;No copies configuraciones antiguas sin comprobar la versión&lt;/b&gt;
  &lt;p&gt;
    Muchos tutoriales de Phaser que todavía aparecen en Google fueron escritos para Phaser 2 o versiones
    tempranas de Phaser 3. El concepto puede seguir siendo válido, pero imports, renderer, plugins y tooling
    pueden haber cambiado.
  &lt;/p&gt;
&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;b&gt;Abrir index.html directamente:&lt;/b&gt; usa siempre el servidor de Vite durante desarrollo.&lt;/li&gt;
  &lt;li&gt;&lt;b&gt;Rutas incorrectas:&lt;/b&gt; revisa dónde viven realmente los assets.&lt;/li&gt;
  &lt;li&gt;&lt;b&gt;Una Scene gigante:&lt;/b&gt; extrae sistemas cuando la lógica empiece a crecer.&lt;/li&gt;
  &lt;li&gt;&lt;b&gt;Instalar muchas librerías desde el inicio:&lt;/b&gt; primero consigue un game loop funcionando.&lt;/li&gt;
  &lt;li&gt;&lt;b&gt;Ignorar TypeScript:&lt;/b&gt; evita abusar de &lt;code&gt;any&lt;/code&gt;; el tipado es parte de la ventaja.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Qué tenemos hasta ahora&lt;/h2&gt;

&lt;p&gt;
Con muy poco código ya tenemos una base moderna:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Phaser 4 ejecutándose como motor del juego.&lt;/li&gt;
  &lt;li&gt;TypeScript para escribir código con tipos.&lt;/li&gt;
  &lt;li&gt;Vite como servidor y sistema de build.&lt;/li&gt;
  &lt;li&gt;Una Scene independiente.&lt;/li&gt;
  &lt;li&gt;Una estructura preparada para assets, sistemas y UI.&lt;/li&gt;
  &lt;li&gt;Un build de producción listo para desplegar.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;jh-article-current&quot;&gt;
  &lt;b&gt;Siguiente tutorial&lt;/b&gt;
  &lt;p&gt;
    El siguiente paso será hacer que este canvas se adapte correctamente a móviles y escritorio sin
    deformar el juego. Ahí veremos resolución lógica, resize, relación de aspecto y HUD responsive.
  &lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;Referencias oficiales&lt;/h2&gt;

&lt;div class=&quot;jh-article-links&quot;&gt;
  &lt;a href=&quot;https://phaser.io/phaser4&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;
    Phaser 4
  &lt;/a&gt;

  &lt;a href=&quot;https://phaser.io/tutorials/create-game-app&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;
    Create Phaser Game
  &lt;/a&gt;
&lt;/div&gt;

&lt;p class=&quot;jh-article-end&quot;&gt;
Este tutorial utiliza el flujo actual de Phaser 4 y está pensado como punto de partida para los próximos ejemplos del blog.
&lt;/p&gt;</description><link>http://juegosenhtml5.blogspot.com/2026/08/phaser-4-vite-typescript-desde-cero.html</link><author>noreply@blogger.com (namzug)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiGMCMEkCUU49BxM0C9Idj8xgKG_JwPP90rMrOGdMDttsmQAF9rg4Ke9V7wlg7QOCnQIu7b1NWKL4v-USlRiZ1rt84iJETdvrdHKmvPrIuxVFBstFmX5IleRnQp28Z-YTRJpuighcVUgbH_IpRRT3YhWE9JP040lK28CwsPabOmuIVQmwEVeVe2AU7EtR2C/s72-c/Creating_tutorial_cover_image_202608241611.jpeg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-2212981333767681873</guid><pubDate>Mon, 02 Feb 2015 15:12:00 +0000</pubDate><atom:updated>2026-08-24T15:05:11.336-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">ganar dinero con fgl</category><category domain="http://www.blogger.com/atom/ns#">HTML5</category><category domain="http://www.blogger.com/atom/ns#">juegos con phaser</category><category domain="http://www.blogger.com/atom/ns#">juegos en html5</category><category domain="http://www.blogger.com/atom/ns#">Phaser</category><category domain="http://www.blogger.com/atom/ns#">Tutoriales</category><title>Cómo monetizar juegos HTML5 con FGL en 2026: qué cambió desde 2014</title><description>&lt;p class=&quot;jh-article-lead&quot;&gt;
FGL fue una de las plataformas más conocidas para distribuir y licenciar juegos web. Este artículo nació en la época de Flash Game License y ha sido revisado para separar claramente el proceso histórico de 2014 de lo que la plataforma documenta actualmente.
&lt;/p&gt;

&lt;div class=&quot;jh-article-note&quot;&gt;
  &lt;div class=&quot;jh-article-note-icon&quot;&gt;↻&lt;/div&gt;
  &lt;div&gt;
    &lt;strong&gt;Artículo histórico actualizado en agosto de 2026&lt;/strong&gt;
    &lt;p&gt;Conservamos el contexto original, pero ya no presentamos como vigentes los precios, paneles o pasos comerciales que pertenecían al antiguo FGL.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;a name=&#39;more&#39;&gt;&lt;/a&gt;

&lt;h2&gt;¿Qué es FGL?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;FGL&lt;/strong&gt; nació como &lt;em&gt;Flash Game License&lt;/em&gt;, una plataforma que ayudaba a conectar desarrolladores de juegos Flash con publishers interesados en comprar o licenciar esos juegos.&lt;/p&gt;

&lt;p&gt;Con la desaparición de Flash, la marca evolucionó hacia otras tecnologías y continúa vinculada a juegos para navegador.&lt;/p&gt;

&lt;div class=&quot;jh-article-links&quot;&gt;
  &lt;a href=&quot;https://fgl.com/&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Visitar FGL&lt;/a&gt;
  &lt;a href=&quot;https://fgl.com/sdk-guide.html&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Guía del SDK&lt;/a&gt;
  &lt;a href=&quot;https://fgl.com/contact-us.html&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Contacto&lt;/a&gt;
&lt;/div&gt;

&lt;h2&gt;Qué cambió desde el artículo original&lt;/h2&gt;
&lt;div class=&quot;jh-compare-grid&quot;&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;2014&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Game Library y Game Shop.&lt;/li&gt;
      &lt;li&gt;Licencias y patrocinio como eje principal.&lt;/li&gt;
      &lt;li&gt;Flujo de publicación mediante paneles separados.&lt;/li&gt;
      &lt;li&gt;Pruebas manuales de resoluciones y dispositivos.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;2026&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Portal activo de juegos web.&lt;/li&gt;
      &lt;li&gt;SDK JavaScript para publicidad.&lt;/li&gt;
      &lt;li&gt;Integración documentada para HTML5 y motores web.&lt;/li&gt;
      &lt;li&gt;Los términos comerciales deben verificarse directamente.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;jh-legacy-box&quot;&gt;
  &lt;strong&gt;Importante&lt;/strong&gt;
  &lt;p&gt;No recomiendo seguir literalmente el antiguo proceso de &lt;em&gt;Game Library → New Game → Game Shop&lt;/em&gt;, ni asumir que siguen vigentes mínimos de licencia o condiciones comerciales descritas hace más de una década.&lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;Flujo actual de monetización con el SDK&lt;/h2&gt;
&lt;ol class=&quot;jh-step-list&quot;&gt;
  &lt;li&gt;&lt;div&gt;&lt;strong&gt;Prepara el juego web&lt;/strong&gt;Verifica rendimiento, input, audio, responsive y comportamiento al perder el foco.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;strong&gt;Configura la integración&lt;/strong&gt;Utiliza la clave e instrucciones proporcionadas para tu juego.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;strong&gt;Inicializa el SDK&lt;/strong&gt;La integración documenta un proceso de inicialización antes de solicitar publicidad.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;strong&gt;Solicita anuncios&lt;/strong&gt;Colócalos únicamente en pausas naturales o como recompensas voluntarias.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;strong&gt;Maneja los fallos&lt;/strong&gt;El juego debe seguir funcionando aunque el anuncio falle o sea bloqueado.&lt;/div&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;Ejemplo mínimo&lt;/h2&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;span&gt;Estructura básica&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;async function iniciarFGL() {
  if (typeof FGL === &quot;undefined&quot;) {
    return;
  }

  const result = await FGL.init(&quot;TU_CLAVE_DE_FGL&quot;);

  if (!result.success) {
    return;
  }
}

async function mostrarAnuncio() {
  const status = await FGL.Ads.request(&quot;midgame&quot;);

  if (status === &quot;success&quot;) {
    console.log(&quot;Anuncio mostrado&quot;);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;Preparar correctamente un juego web&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;Diseño responsive y HUD legible.&lt;/li&gt;
  &lt;li&gt;Mouse, teclado y/o touch según el juego.&lt;/li&gt;
  &lt;li&gt;Landscape y portrait cuando sean necesarios.&lt;/li&gt;
  &lt;li&gt;Carga inicial optimizada.&lt;/li&gt;
  &lt;li&gt;Audio compatible con las restricciones del navegador.&lt;/li&gt;
  &lt;li&gt;Pausa y reanudación al cambiar de pestaña.&lt;/li&gt;
  &lt;li&gt;Fallback cuando no existe publicidad disponible.&lt;/li&gt;
  &lt;li&gt;Pruebas en dispositivos reales.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Archivo histórico&lt;/h2&gt;
&lt;figure class=&quot;jh-article-figure&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBiwx9HFb4Fg56or4lDg2MC26hvIPfxfn1DQY99CqSKAj2hpLOpVVZR4LpyDhi-fyomzBYAjrFTm-ViciseOSCmVK9waRHBKHd3pKDzhdOJ_Xh7eCadQOo_nqCM-urcb0jVPBgdyor2PE/s1600/dineroconfgl.png&quot;&gt;
    &lt;img alt=&quot;Captura histórica del antiguo proceso de FGL&quot; loading=&quot;lazy&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBiwx9HFb4Fg56or4lDg2MC26hvIPfxfn1DQY99CqSKAj2hpLOpVVZR4LpyDhi-fyomzBYAjrFTm-ViciseOSCmVK9waRHBKHd3pKDzhdOJ_Xh7eCadQOo_nqCM-urcb0jVPBgdyor2PE/s1600/dineroconfgl.png&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;Captura conservada del artículo original. La interfaz ya no debe interpretarse como guía actual.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;details class=&quot;jh-archive-details&quot;&gt;
  &lt;summary&gt;Ver video histórico del proceso&lt;/summary&gt;
  &lt;div&gt;
    &lt;p&gt;La interfaz del video corresponde al FGL de aquella época.&lt;/p&gt;
    &lt;iframe allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; src=&quot;https://www.youtube.com/embed/pK2nZs-8A5k&quot; title=&quot;Proceso histórico para subir un juego a FGL&quot;&gt;&lt;/iframe&gt;
  &lt;/div&gt;
&lt;/details&gt;
</description><link>http://juegosenhtml5.blogspot.com/2015/02/como-ganar-dinero-con-fgl.html</link><author>noreply@blogger.com (a)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBiwx9HFb4Fg56or4lDg2MC26hvIPfxfn1DQY99CqSKAj2hpLOpVVZR4LpyDhi-fyomzBYAjrFTm-ViciseOSCmVK9waRHBKHd3pKDzhdOJ_Xh7eCadQOo_nqCM-urcb0jVPBgdyor2PE/s72-c/dineroconfgl.png" height="72" width="72"/><thr:total>4</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-5250076902393048626</guid><pubDate>Mon, 26 Jan 2015 14:56:00 +0000</pubDate><atom:updated>2026-08-24T15:05:12.481-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">html5</category><category domain="http://www.blogger.com/atom/ns#">juego con Phaser</category><category domain="http://www.blogger.com/atom/ns#">juego en html5</category><category domain="http://www.blogger.com/atom/ns#">little strong</category><category domain="http://www.blogger.com/atom/ns#">Phaser</category><title>Little Strong: juego HTML5 con Phaser</title><description>&lt;p class=&quot;jh-article-lead&quot;&gt;
Little Strong fue uno de los juegos HTML5 que desarrollé con Phaser durante una etapa en la que estaba explorando cómo crear, distribuir y monetizar juegos directamente para el navegador. Este artículo conserva la historia del proyecto, pero reorganizada y actualizada para que siga teniendo sentido hoy.
&lt;/p&gt;

&lt;div class=&quot;jh-article-note&quot;&gt;
  &lt;div class=&quot;jh-article-note-icon&quot;&gt;↻&lt;/div&gt;
  &lt;div&gt;
    &lt;strong&gt;Artículo original de 2015 · actualizado en 2026&lt;/strong&gt;
    &lt;p&gt;
      El desarrollo de Little Strong y las experiencias con FGL que cuento aquí pertenecen a aquella etapa. Algunas plataformas, procesos de distribución y herramientas han cambiado desde entonces.
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;figure class=&quot;jh-article-figure&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzllM-MvIlQ-F4k_fpiXnvsgGMpXcOGXKFr2XWQGe2TpGPW4SmevWqHnp6iD5vfzvo3pQSebDMSDa9-NcphOOxa51TVe0j8r2KUsLYqb_CHYHbzYysnSlknAGPiUbVH3SjgoYeiKGNnxk/s1600/juego+en+Html5+little+strong.png&quot;&gt;
    &lt;img alt=&quot;Little Strong, juego HTML5 desarrollado con Phaser&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzllM-MvIlQ-F4k_fpiXnvsgGMpXcOGXKFr2XWQGe2TpGPW4SmevWqHnp6iD5vfzvo3pQSebDMSDa9-NcphOOxa51TVe0j8r2KUsLYqb_CHYHbzYysnSlknAGPiUbVH3SjgoYeiKGNnxk/s1600/juego+en+Html5+little+strong.png&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;Little Strong, uno de mis proyectos HTML5 desarrollados con Phaser.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;a name=&#39;more&#39;&gt;&lt;/a&gt;

&lt;h2&gt;Qué era Little Strong&lt;/h2&gt;

&lt;p&gt;
Después de un tiempo alejado del blog, gran parte de mi atención estaba puesta en mejorar dibujo e ilustración digital. Al mismo tiempo ya tenía preparado mi siguiente proyecto: &lt;strong&gt;Little Strong&lt;/strong&gt;, un juego que había mencionado anteriormente pero del que todavía no había explicado el proceso.
&lt;/p&gt;

&lt;p&gt;
A diferencia de &lt;strong&gt;Archaic Roll&lt;/strong&gt;, que publiqué en varios portales para experimentar con tráfico y monetización, con Little Strong seguí un enfoque diferente: preparar el juego para presentarlo a posibles compradores y patrocinadores antes de distribuirlo ampliamente.
&lt;/p&gt;

&lt;p&gt;
En aquel momento una distribución demasiado abierta podía reducir el interés de compradores que buscaban cierta exclusividad. También existía la alternativa de publicar el juego directamente en tiendas y tratar de construir audiencia por cuenta propia. Eran dos estrategias distintas y Little Strong nació dentro de la primera.
&lt;/p&gt;

&lt;h2&gt;Publicar y buscar patrocinio en aquella época&lt;/h2&gt;

&lt;p&gt;
Para Little Strong utilicé &lt;a href=&quot;https://fgl.com/&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;&lt;strong&gt;FGL&lt;/strong&gt;&lt;/a&gt;. Durante aquellos años la plataforma estaba muy vinculada al mercado de licencias, distribución y patrocinio de juegos web, y mi experiencia con sus pagos había sido positiva.
&lt;/p&gt;

&lt;p&gt;
Uno de los retos del proyecto fue conseguir un comportamiento consistente entre distintos dispositivos. El audio, por ejemplo, podía requerir bastante trabajo adicional en determinados navegadores y tablets como Kindle Fire HD.
&lt;/p&gt;

&lt;figure class=&quot;jh-article-figure&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhTovVzHlGpktc1Ct6w_BkYXJqVFRJ7Ep_n7tpBMisp6TNPZSKf8f9Oz8HRqjfqHvW85iZ3c2FjN1Z6AQoLMD78fj5i5JboCAe-j1H7Pro1lKocDUGT1_8GWvT-3juNPilbnZrHwhlGys8/s1600/juego+Phaser.png&quot;&gt;
    &lt;img alt=&quot;Captura de un juego desarrollado con Phaser&quot; loading=&quot;lazy&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhTovVzHlGpktc1Ct6w_BkYXJqVFRJ7Ep_n7tpBMisp6TNPZSKf8f9Oz8HRqjfqHvW85iZ3c2FjN1Z6AQoLMD78fj5i5JboCAe-j1H7Pro1lKocDUGT1_8GWvT-3juNPilbnZrHwhlGys8/s1600/juego+Phaser.png&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;Una de las capturas originales del proyecto y de su proceso de publicación.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;div class=&quot;jh-article-current&quot;&gt;
  &lt;strong&gt;Contexto actual · 2026&lt;/strong&gt;
  &lt;p&gt;
    FGL continúa existiendo, aunque hoy su sitio se presenta principalmente como un portal de juegos online y browser games. Por eso esta parte del artículo debe entenderse como documentación histórica de cómo trabajábamos y distribuíamos juegos HTML5 en 2015, no como una guía actual de publicación.
  &lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;Cómo funcionaba el juego&lt;/h2&gt;

&lt;p&gt;
La mecánica era intencionalmente sencilla. El personaje avanzaba y el jugador debía reaccionar a los obstáculos utilizando &lt;strong&gt;click o touch para saltar&lt;/strong&gt;. Algunos elementos podían utilizarse como apoyo para alcanzar mayor altura, otros podían derribarse golpeándolos y durante el recorrido también era posible recoger monedas.
&lt;/p&gt;

&lt;p&gt;
Phaser permitió construir este tipo de interacción con bastante rapidez. El objetivo no era crear un sistema enorme, sino conseguir un juego fácil de entender, compatible con navegador y suficientemente sólido para probarlo en distintos dispositivos.
&lt;/p&gt;

&lt;figure class=&quot;jh-article-figure&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMPEAgIiMjPz0piJHN8Ng_GO2-6KBtc77b9-h52PGQUCnwDFqj5YPYy8eDeuEFPSrxQXj9SAVkZlXHgpBSP78BtL379nNwpSDKgbBK58SEj4GXL8GL2Obfc3E1nLYcMuQjk4IhdHuURO4/s1600/Little+strong.png&quot;&gt;
    &lt;img alt=&quot;Gameplay de Little Strong&quot; loading=&quot;lazy&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMPEAgIiMjPz0piJHN8Ng_GO2-6KBtc77b9-h52PGQUCnwDFqj5YPYy8eDeuEFPSrxQXj9SAVkZlXHgpBSP78BtL379nNwpSDKgbBK58SEj4GXL8GL2Obfc3E1nLYcMuQjk4IhdHuURO4/s1600/Little+strong.png&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;Gameplay original de Little Strong.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2&gt;Herramientas que utilicé&lt;/h2&gt;

&lt;div class=&quot;jh-article-grid&quot;&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;strong&gt;Phaser&lt;/strong&gt;
    &lt;span&gt;Framework utilizado para construir la lógica y las mecánicas del juego en el navegador.&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;strong&gt;Inkscape&lt;/strong&gt;
    &lt;span&gt;Herramienta con la que preparé los gráficos vectoriales del proyecto.&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;
    &lt;strong&gt;ShoeBox&lt;/strong&gt;
    &lt;span&gt;Lo utilicé dentro del flujo de trabajo para preparar y organizar recursos de animación.&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;
El intro de audio lo hice yo y la música de fondo procedía de recursos que permitían su utilización. El objetivo era mantener un proceso de producción accesible y resolver internamente la mayor parte de los elementos del juego.
&lt;/p&gt;

&lt;div class=&quot;jh-article-current&quot;&gt;
  &lt;strong&gt;Phaser hoy&lt;/strong&gt;
  &lt;p&gt;
    El ecosistema ha avanzado enormemente desde este proyecto. Phaser mantiene la rama Phaser 3 y, en 2026, Phaser 4 ya es una línea estable del framework. Si reconstruyera Little Strong hoy, conservaría la idea sencilla del juego, pero actualizaría el proyecto al tooling y APIs modernas de Phaser.
  &lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;Mirando Little Strong desde 2026&lt;/h2&gt;

&lt;p&gt;
Lo más interesante de recuperar este proyecto no es solamente el juego final, sino observar cómo era desarrollar para HTML5 en aquel momento: compatibilidad móvil más complicada, audio inconsistente entre dispositivos, distribución mediante portales y un ecosistema que todavía estaba definiendo muchas de las prácticas que hoy damos por sentadas.
&lt;/p&gt;

&lt;p&gt;
Little Strong representa justamente esa etapa del desarrollo web: crear algo pequeño, hacerlo funcionar en navegador, probar distintas formas de distribución y aprender del proceso.
&lt;/p&gt;

&lt;p class=&quot;jh-article-end&quot;&gt;
Si vuelvo a publicar una versión jugable o recupero los archivos originales del proyecto, actualizaré esta entrada con una demo y más detalles técnicos.
&lt;/p&gt;
</description><link>http://juegosenhtml5.blogspot.com/2015/01/little-strong-juego-en-html5.html</link><author>noreply@blogger.com (a)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzllM-MvIlQ-F4k_fpiXnvsgGMpXcOGXKFr2XWQGe2TpGPW4SmevWqHnp6iD5vfzvo3pQSebDMSDa9-NcphOOxa51TVe0j8r2KUsLYqb_CHYHbzYysnSlknAGPiUbVH3SjgoYeiKGNnxk/s72-c/juego+en+Html5+little+strong.png" height="72" width="72"/><thr:total>1</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-2748101829778941311</guid><pubDate>Wed, 31 Dec 2014 20:38:00 +0000</pubDate><atom:updated>2026-08-24T15:05:13.609-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">feliz año</category><category domain="http://www.blogger.com/atom/ns#">HTML5</category><category domain="http://www.blogger.com/atom/ns#">juegos con phaser</category><category domain="http://www.blogger.com/atom/ns#">Phaser</category><title>Phaser 2014: minijuego de fuerza y lanzamiento</title><description>&lt;p class=&quot;jh-article-lead&quot;&gt;
Esta entrada cerró el primer año del blog repasando el código del minijuego de lanzamiento que habíamos construido con Phaser. Además de ser un tutorial, es una fotografía bastante clara de cómo escribíamos juegos HTML5 con Phaser 2 en 2014.
&lt;/p&gt;

&lt;div class=&quot;jh-article-note&quot;&gt;
  &lt;div class=&quot;jh-article-note-icon&quot;&gt;↻&lt;/div&gt;
  &lt;div&gt;
    &lt;strong&gt;Archivo técnico de 2014 · reorganizado en 2026&lt;/strong&gt;
    &lt;p&gt;Conservo las ideas y el código original, pero los presento como documentación de Phaser 2. No deben confundirse con la API actual de Phaser.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;figure class=&quot;jh-article-figure&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi-tXmPRVKz0D2AAN7pbXJFydKBzhwIHMUQt0yN0d1XTljOd9qRmDaW7vFW9stLCGaEIkn1DYCqfTZwqDURqniT7YkW8NgOozuLeBBO0tmUWRrz0i7mivpk4SwB5xYfL1jdt3QUas1JXnU/s1600/phaser+2014.jpg&quot;&gt;
    &lt;img alt=&quot;Phaser 2014, tutorial histórico de juegos HTML5&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi-tXmPRVKz0D2AAN7pbXJFydKBzhwIHMUQt0yN0d1XTljOd9qRmDaW7vFW9stLCGaEIkn1DYCqfTZwqDURqniT7YkW8NgOozuLeBBO0tmUWRrz0i7mivpk4SwB5xYfL1jdt3QUas1JXnU/s1600/phaser+2014.jpg&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;Imagen original de la entrada con la que cerramos 2014.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;a name=&#39;more&#39;&gt;&lt;/a&gt;

&lt;h2&gt;Un año aprendiendo Phaser&lt;/h2&gt;
&lt;p&gt;Cuando publiqué esta entrada había pasado menos de un año desde el inicio del blog. Habíamos experimentado con física, sprites, input, grupos, partículas y pequeños prototipos. El objetivo de este último artículo era repasar el funcionamiento del minijuego de lanzamiento y explicar sus piezas con más calma.&lt;/p&gt;

&lt;div class=&quot;jh-version-badge is-legacy&quot;&gt;Phaser 2 · diciembre de 2014&lt;/div&gt;

&lt;h2&gt;1. Variables e instancia del juego&lt;/h2&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;span&gt;Phaser 2&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;var estrellas, balon, stars, flecha;

var game = new Phaser.Game(
  400,
  400,
  Phaser.AUTO,
  &quot;ejemplo&quot;,
  {
    preload: preload,
    create: create,
    update: update
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;2. Cargar los recursos&lt;/h2&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;preload()&lt;/strong&gt;&lt;span&gt;Assets&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;function preload() {
  game.load.image(&quot;star&quot;, &quot;S.png&quot;);
  game.load.image(&quot;ball&quot;, &quot;b.png&quot;);
  game.load.image(&quot;a&quot;, &quot;arrow.png&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;3. Crear física, objetivos e input&lt;/h2&gt;
&lt;p&gt;La escena inicializaba Arcade Physics, creaba un grupo para las estrellas, añadía la esfera y registraba dos eventos: pulsar y soltar. Las variables &lt;code&gt;presionado&lt;/code&gt;, &lt;code&gt;tiempoSostenido&lt;/code&gt; y &lt;code&gt;estado&lt;/code&gt; controlaban la pequeña máquina de estados del lanzamiento.&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;create()&lt;/strong&gt;&lt;span&gt;Fragmento reorganizado&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;function create() {
  game.stage.setBackgroundColor(0x22aaff);
  game.physics.startSystem(Phaser.Physics.ARCADE);

  stars = game.add.group();
  stars.enableBody = true;
  stars.physicsBodyType = Phaser.Physics.ARCADE;

  balon = game.add.sprite(200, 350, &quot;ball&quot;);
  game.physics.arcade.enable(balon);

  balon.body.collideWorldBounds = true;
  balon.body.bounce.setTo(1, 1);

  game.input.onDown.add(presiona, this);
  game.input.onUp.add(suelta, this);

  presionado = false;
  tiempoSostenido = 0;
  estado = null;

  comenzar(0);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;4. Medir la fuerza de lanzamiento&lt;/h2&gt;
&lt;p&gt;El prototipo convertía el tiempo que manteníamos presionado el mouse en un valor de fuerza que subía y bajaba dentro de un rango. Era una forma compacta de crear un medidor sin necesitar una interfaz compleja.&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;Input&lt;/strong&gt;&lt;span&gt;Presionar, soltar y fuerza&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;function presiona(pointer) {
  presionado = true;
}

function suelta(pointer) {
  presionado = false;
}

function fuerza() {
  return Math.abs(
    ((game.time.now - tiempoSostenido + 600) % 1000) - 600
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;5. La flecha como hijo de la esfera&lt;/h2&gt;
&lt;p&gt;La flecha se añadía como &lt;em&gt;child&lt;/em&gt; de la esfera para mantener una posición relativa al proyectil. El artículo original ya señalaba un defecto importante: el cuerpo físico de la esfera no coincidía perfectamente con su apariencia, por lo que la rotación visual podía resultar extraña.&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;Jerarquía&lt;/strong&gt;&lt;span&gt;Phaser 2 / PIXI&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;function crearFlecha() {
  flecha = game.add.sprite(0, 0, &quot;a&quot;);

  flecha.anchor.setTo(0.5, 0.5);
  flecha.pivot.x = -18;
  flecha.pivot.y = 30;

  balon.addChild(flecha);
}

function removerFlecha() {
  balon.removeChild(flecha);
  flecha.destroy();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;6. Estados del lanzamiento&lt;/h2&gt;
&lt;p&gt;El juego alternaba entre dos estados simples: preparar la flecha y dejar que la esfera se moviera. Cuando el proyectil volvía prácticamente a detenerse, se preparaba el siguiente lanzamiento.&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;Estado&lt;/strong&gt;&lt;span&gt;Phaser 2&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;function comenzar(nuevoEstado) {
  if (estado !== nuevoEstado) {
    if (estado === 1) {
      removerFlecha();
    } else if (estado === 0) {
      balon.body.velocity.setTo(0, 0);
      crearFlecha();
      tiempoSostenido = 0;
    }

    estado = nuevoEstado;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;7. Update: apuntar, lanzar y recoger estrellas&lt;/h2&gt;
&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;update()&lt;/strong&gt;&lt;span&gt;Lógica central&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;function update() {
  game.physics.arcade.overlap(
    balon,
    stars,
    destruye,
    null,
    this
  );

  if (estado === 0) {
    if (
      Math.abs(balon.body.velocity.x) &amp;lt; 10 &amp;amp;&amp;amp;
      Math.abs(balon.body.velocity.y) &amp;lt; 10
    ) {
      comenzar(1);
    }
  } else if (estado === 1) {
    flecha.rotation =
      game.math.angleBetween(
        balon.body.x,
        balon.body.y,
        game.input.x,
        game.input.y
      ) + Math.PI / 2;

    if (presionado) {
      if (tiempoSostenido === 0) {
        tiempoSostenido = game.time.now;
      } else {
        flecha.scale.y =
          1.0 + fuerza() / 500 * 2;
      }
    } else if (tiempoSostenido !== 0) {
      balon.body.velocity =
        game.physics.arcade.accelerationFromRotation(
          flecha.rotation - Math.PI / 2,
          fuerza() + 50
        );

      comenzar(0);
    }
  }
}

function destruye(a, b) {
  b.kill();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&quot;jh-article-current&quot;&gt;
  &lt;strong&gt;Qué sigue siendo útil hoy&lt;/strong&gt;
  &lt;p&gt;Las APIs concretas han cambiado, pero las ideas siguen siendo muy actuales: separar estados, medir input, convertirlo en fuerza, calcular un ángulo, aplicar una velocidad y comprobar colisiones. Esa es justamente la parte que vale la pena conservar de este tutorial.&lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;Phaser hoy&lt;/h2&gt;
&lt;p&gt;El Phaser que utilizábamos en este código pertenece a otra generación del framework. Phaser continúa activo más de una década después, con Phaser 3.90 disponible y una línea estable de Phaser 4. Si reconstruimos este ejemplo, debería hacerse como un tutorial nuevo en lugar de intentar adaptar línea por línea la API de 2014.&lt;/p&gt;

&lt;p class=&quot;jh-article-end&quot;&gt;Esta entrada queda como archivo del primer año del blog y como punto de partida para una futura reconstrucción moderna del mismo minijuego.&lt;/p&gt;
</description><link>http://juegosenhtml5.blogspot.com/2014/12/phaser-juegos.html</link><author>noreply@blogger.com (a)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi-tXmPRVKz0D2AAN7pbXJFydKBzhwIHMUQt0yN0d1XTljOd9qRmDaW7vFW9stLCGaEIkn1DYCqfTZwqDURqniT7YkW8NgOozuLeBBO0tmUWRrz0i7mivpk4SwB5xYfL1jdt3QUas1JXnU/s72-c/phaser+2014.jpg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-8402723299206787448</guid><pubDate>Thu, 18 Dec 2014 23:02:00 +0000</pubDate><atom:updated>2026-08-24T15:05:14.748-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">hacer juegos con phaser</category><category domain="http://www.blogger.com/atom/ns#">HTML5</category><category domain="http://www.blogger.com/atom/ns#">minijuego con phaser</category><category domain="http://www.blogger.com/atom/ns#">Phaser</category><title>Minijuego en Phaser: prototipo de lanzamiento y medidor de fuerza</title><description>&lt;p class=&quot;jh-article-lead&quot;&gt;
Este minijuego nació como un prototipo rápido en Phaser: una esfera, una dirección de lanzamiento, un medidor de fuerza y varias estrellas que el jugador debía recoger. El objetivo no era terminar un juego completo, sino validar primero una mecánica.
&lt;/p&gt;

&lt;div class=&quot;jh-article-note&quot;&gt;
  &lt;div class=&quot;jh-article-note-icon&quot;&gt;↻&lt;/div&gt;
  &lt;div&gt;
    &lt;strong&gt;Artículo original de 2014 · revisado en 2026&lt;/strong&gt;
    &lt;p&gt;El concepto de prototipar sigue siendo válido. La implementación original, sin embargo, pertenece a Phaser 2 y el demo estaba alojado en el antiguo hosting web de Google Drive.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;figure class=&quot;jh-article-figure&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFffagJE14hjtwmVRVJ6LKJqYXojn-2jWgmOSTR8fPsUoQwfMLYal3hoTzjDGU1w_2vBemFyDazXX7UpbVO-1QjUrtKkF7tQotbaiWU9GVzwX-VLMjkIjWAbc9SazVJVhGch5sNr-e100/s1600/hacerjuegosphaser.png&quot;&gt;
    &lt;img alt=&quot;Prototipo del minijuego creado con Phaser&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFffagJE14hjtwmVRVJ6LKJqYXojn-2jWgmOSTR8fPsUoQwfMLYal3hoTzjDGU1w_2vBemFyDazXX7UpbVO-1QjUrtKkF7tQotbaiWU9GVzwX-VLMjkIjWAbc9SazVJVhGch5sNr-e100/s1600/hacerjuegosphaser.png&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;El prototipo original: lanzamiento, indicador de fuerza y objetivos.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;a name=&#39;more&#39;&gt;&lt;/a&gt;

&lt;h2&gt;La idea del prototipo&lt;/h2&gt;
&lt;p&gt;La mecánica era sencilla: lanzar un objeto hacia la parte superior de la pantalla para recoger estrellas. Este tipo de interacción aparece en muchos juegos porque combina una acción fácil de entender con variables suficientes para experimentar: dirección, potencia, gravedad, rebote y objetivos.&lt;/p&gt;

&lt;p&gt;Antes de construir arte final, niveles o sistemas más grandes, quería comprobar si el lanzamiento era divertido. Esa sigue siendo una buena forma de trabajar: &lt;strong&gt;primero validar el gameplay y después invertir en producción&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;Qué quería probar&lt;/h2&gt;
&lt;div class=&quot;jh-article-grid&quot;&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;&lt;strong&gt;Dirección&lt;/strong&gt;&lt;span&gt;Una flecha indicaba hacia dónde sería lanzada la esfera.&lt;/span&gt;&lt;/div&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;&lt;strong&gt;Fuerza&lt;/strong&gt;&lt;span&gt;El tiempo que el jugador mantenía presionado modificaba la potencia.&lt;/span&gt;&lt;/div&gt;
  &lt;div class=&quot;jh-article-card&quot;&gt;&lt;strong&gt;Objetivos&lt;/strong&gt;&lt;span&gt;Las estrellas servían como primera condición de interacción y recolección.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;Por qué prototipar con Phaser&lt;/h2&gt;
&lt;p&gt;Phaser permitía poner rápidamente algo en movimiento y comprobar la idea directamente en el navegador. Ese valor continúa vigente: Phaser sigue siendo un framework activo para juegos HTML5, aunque las APIs modernas son muy diferentes de las utilizadas en este prototipo.&lt;/p&gt;

&lt;div class=&quot;jh-version-badge is-legacy&quot;&gt;Phaser 2 · código histórico&lt;/div&gt;

&lt;p&gt;En la implementación original todavía había problemas evidentes, como la forma en que la flecha giraba con respecto a la esfera. El objetivo del prototipo era precisamente encontrar esos defectos antes de construir el resto del juego.&lt;/p&gt;

&lt;div class=&quot;jh-demo-status&quot;&gt;
  &lt;div class=&quot;jh-demo-status-icon&quot;&gt;!&lt;/div&gt;
  &lt;div&gt;
    &lt;strong&gt;El demo original ya no está disponible&lt;/strong&gt;
    &lt;p&gt;Estaba alojado mediante &lt;code&gt;googledrive.com/host/...&lt;/code&gt;. Google retiró ese servicio de hosting en 2016. Si recuperamos los archivos originales, el demo debería republicarse en un hosting web actual.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;Qué reconstruiría hoy&lt;/h2&gt;
&lt;ol class=&quot;jh-step-list&quot;&gt;
  &lt;li&gt;&lt;div&gt;&lt;strong&gt;Escena mínima&lt;/strong&gt;Crear esfera, objetivos y una cámara sencilla.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;strong&gt;Pointer input&lt;/strong&gt;Registrar cuándo comienza y termina la pulsación.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;strong&gt;Potencia&lt;/strong&gt;Convertir el tiempo de pulsación en una fuerza limitada.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;strong&gt;Dirección&lt;/strong&gt;Calcular el ángulo entre el proyectil y el puntero.&lt;/div&gt;&lt;/li&gt;
  &lt;li&gt;&lt;div&gt;&lt;strong&gt;Lanzamiento&lt;/strong&gt;Aplicar velocidad en lugar de acoplar la solución a APIs antiguas de Phaser 2.&lt;/div&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;jh-article-current&quot;&gt;
  &lt;strong&gt;Idea que sigue siendo válida&lt;/strong&gt;
  &lt;p&gt;Un prototipo no tiene que verse perfecto. Tiene que responder una pregunta concreta: ¿esta mecánica funciona y merece que sigamos construyendo sobre ella?&lt;/p&gt;
&lt;/div&gt;
</description><link>http://juegosenhtml5.blogspot.com/2014/12/minijuego-en-phaser.html</link><author>noreply@blogger.com (a)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFffagJE14hjtwmVRVJ6LKJqYXojn-2jWgmOSTR8fPsUoQwfMLYal3hoTzjDGU1w_2vBemFyDazXX7UpbVO-1QjUrtKkF7tQotbaiWU9GVzwX-VLMjkIjWAbc9SazVJVhGch5sNr-e100/s72-c/hacerjuegosphaser.png" height="72" width="72"/><thr:total>4</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-6638622457873992599</guid><pubDate>Wed, 10 Dec 2014 16:25:00 +0000</pubDate><atom:updated>2026-08-24T15:05:15.914-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">fgl</category><category domain="http://www.blogger.com/atom/ns#">HTML5</category><category domain="http://www.blogger.com/atom/ns#">juegos con phaser</category><category domain="http://www.blogger.com/atom/ns#">Phaser</category><category domain="http://www.blogger.com/atom/ns#">plugins para phaser</category><title>Plugins en Phaser: del antiguo phaser-debug al sistema moderno</title><description>&lt;p class=&quot;jh-article-lead&quot;&gt;
La comunidad siempre ha sido una de las fortalezas de Phaser. En 2014 escribí esta entrada para mostrar &lt;code&gt;phaser-debug&lt;/code&gt;, un plugin que añadía paneles de rendimiento y un árbol de escena directamente al juego. Hoy esa implementación pertenece a Phaser 2, pero el concepto de extender el engine mediante plugins sigue existiendo.
&lt;/p&gt;

&lt;div class=&quot;jh-article-note&quot;&gt;
  &lt;div class=&quot;jh-article-note-icon&quot;&gt;↻&lt;/div&gt;
  &lt;div&gt;
    &lt;strong&gt;Tutorial histórico de Phaser 2 · actualizado para 2026&lt;/strong&gt;
    &lt;p&gt;El código original se conserva como referencia. No debes copiar &lt;code&gt;game.add.plugin(Phaser.Plugin.Debug)&lt;/code&gt; en un proyecto moderno de Phaser 3 o Phaser 4.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;figure class=&quot;jh-article-figure&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg1ft1WFuEOUTSg8_JExjRfhqS26nxwWWtDgrZiXAuql0O0sTDdjEwvIDQUjYFezZ5AbKxKVEzlVRAtQW0uqrp17cvqiBMosorGXMZkiLWcimdwqYyl-uKpbU3StX8_zk5tCshcgEK-Wms/s1600/juegos+con+html5.png&quot;&gt;
    &lt;img alt=&quot;Plugins y herramientas para Phaser&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg1ft1WFuEOUTSg8_JExjRfhqS26nxwWWtDgrZiXAuql0O0sTDdjEwvIDQUjYFezZ5AbKxKVEzlVRAtQW0uqrp17cvqiBMosorGXMZkiLWcimdwqYyl-uKpbU3StX8_zk5tCshcgEK-Wms/s1600/juegos+con+html5.png&quot; /&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;Imagen original de la entrada sobre plugins de Phaser.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;a name=&#39;more&#39;&gt;&lt;/a&gt;

&lt;h2&gt;Qué era phaser-debug&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;phaser-debug&lt;/code&gt; fue un módulo creado para Phaser 2 que mostraba información de rendimiento y diagnóstico directamente sobre el juego. Permitía inspeccionar tiempos de actualización y visualizar la jerarquía de objetos sin depender únicamente de las herramientas del navegador.&lt;/p&gt;

&lt;div class=&quot;jh-version-badge is-legacy&quot;&gt;Phaser 2 · phaser-debug 1.x&lt;/div&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;Phaser 2&lt;/strong&gt;&lt;span&gt;Fragmento histórico&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;// Archivo externo usado entonces
// phaser-debug.js

var game = new Phaser.Game(
  500,
  500,
  Phaser.AUTO,
  &quot;caja&quot;,
  {
    preload: preload,
    create: create,
    update: update
  }
);

function create() {
  game.add.plugin(Phaser.Plugin.Debug);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&quot;jh-legacy-box&quot;&gt;
  &lt;strong&gt;No utilizar como código moderno&lt;/strong&gt;
  &lt;p&gt;Ese plugin pertenece al ecosistema de Phaser 2 y su última actividad publicada es de hace muchos años. Mantengo el fragmento porque documenta cómo trabajábamos entonces.&lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;Cómo funcionan los plugins en Phaser moderno&lt;/h2&gt;
&lt;p&gt;Phaser 3 dispone de un &lt;strong&gt;PluginManager&lt;/strong&gt; y distingue principalmente entre plugins globales y plugins de escena. Un plugin puede cargarse durante el preload, incluirse en la configuración del juego o instalarse desde una escena.&lt;/p&gt;

&lt;div class=&quot;jh-code-block&quot;&gt;
  &lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;Phaser 3&lt;/strong&gt;&lt;span&gt;Ejemplo conceptual moderno&lt;/span&gt;&lt;/div&gt;
  &lt;pre&gt;&lt;code&gt;class MiPlugin extends Phaser.Plugins.BasePlugin {
  start() {
    console.log(&quot;Plugin iniciado&quot;);
  }
}

const config = {
  type: Phaser.AUTO,
  width: 800,
  height: 450,

  plugins: {
    global: [
      {
        key: &quot;MiPlugin&quot;,
        plugin: MiPlugin,
        start: true
      }
    ]
  },

  scene: Juego
};

new Phaser.Game(config);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;Phaser 2 frente a Phaser 3/4&lt;/h2&gt;
&lt;div class=&quot;jh-compare-grid&quot;&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;Entrada original&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code&gt;game.add.plugin(...)&lt;/code&gt;&lt;/li&gt;
      &lt;li&gt;Phaser.Plugin.Debug.&lt;/li&gt;
      &lt;li&gt;PIXI y World descritos según Phaser 2.&lt;/li&gt;
      &lt;li&gt;Demo alojado en Google Drive.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
  &lt;div class=&quot;jh-compare&quot;&gt;
    &lt;h3&gt;Phaser moderno&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;PluginManager.&lt;/li&gt;
      &lt;li&gt;Global Plugins y Scene Plugins.&lt;/li&gt;
      &lt;li&gt;Game Config y Scene Config.&lt;/li&gt;
      &lt;li&gt;Tooling y arquitectura actuales.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;¿Y el antiguo panel de Performance?&lt;/h2&gt;
&lt;p&gt;En aquella entrada me interesaba especialmente poder ver FPS, tiempos de actualización y el árbol de objetos. El objetivo sigue siendo válido: medir el rendimiento dentro del contexto real del juego, identificar qué sistemas consumen tiempo y entender qué existe en la escena.&lt;/p&gt;

&lt;p&gt;Lo que ha cambiado es la herramienta concreta. No conviene construir un proyecto actual alrededor de &lt;code&gt;phaser-debug&lt;/code&gt;; utiliza las herramientas, APIs y plugins compatibles con la versión de Phaser que realmente esté ejecutando tu proyecto.&lt;/p&gt;

&lt;div class=&quot;jh-demo-status&quot;&gt;
  &lt;div class=&quot;jh-demo-status-icon&quot;&gt;!&lt;/div&gt;
  &lt;div&gt;
    &lt;strong&gt;Demo histórico retirado&lt;/strong&gt;
    &lt;p&gt;La demostración original se servía mediante Google Drive Web Hosting, un servicio que dejó de funcionar en 2016.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;jh-article-current&quot;&gt;
  &lt;strong&gt;Estado de Phaser en 2026&lt;/strong&gt;
  &lt;p&gt;Phaser continúa activamente mantenido. Phaser 3.90.0 sigue disponible y Phaser 4 ya cuenta con releases estables. Antes de instalar cualquier plugin, comprueba explícitamente para qué rama y versión fue creado.&lt;/p&gt;
&lt;/div&gt;
</description><link>http://juegosenhtml5.blogspot.com/2014/12/plugins-para-phaser.html</link><author>noreply@blogger.com (a)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg1ft1WFuEOUTSg8_JExjRfhqS26nxwWWtDgrZiXAuql0O0sTDdjEwvIDQUjYFezZ5AbKxKVEzlVRAtQW0uqrp17cvqiBMosorGXMZkiLWcimdwqYyl-uKpbU3StX8_zk5tCshcgEK-Wms/s72-c/juegos+con+html5.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-1478345084854529763</guid><pubDate>Thu, 04 Dec 2014 19:43:00 +0000</pubDate><atom:updated>2026-08-24T15:05:16.968-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">HTML5</category><category domain="http://www.blogger.com/atom/ns#">juegos con html5</category><category domain="http://www.blogger.com/atom/ns#">particulas</category><category domain="http://www.blogger.com/atom/ns#">phaser</category><category domain="http://www.blogger.com/atom/ns#">Tutoriales</category><title>Cómo colisionar partículas en Phaser</title><description>&lt;p class=&quot;jh-article-lead&quot;&gt;En esta ocasion veamos como colisionar particulas usando fisica y creando una reaccion despues de dicha colision.&lt;/p&gt;&lt;div class=&quot;jh-article-note&quot;&gt;&lt;div class=&quot;jh-article-note-icon&quot;&gt;↻&lt;/div&gt;&lt;div&gt;&lt;strong&gt;Artículo original de 2014 · revisado en 2026&lt;/strong&gt;&lt;p&gt;Esta entrada forma parte del archivo histórico del blog. Se ha limpiado su HTML y conservado el contenido original; cuando utiliza APIs antiguas, se indica el contexto para no confundirlas con las versiones modernas.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;jh-version-badge is-legacy&quot;&gt;Phaser clásico · código histórico&lt;/div&gt;&lt;figure class=&quot;jh-article-figure&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2LXO5AM09tiHYIhkPpPlS0ngZhGfffYzP6jmRyGmxmIRy7Csiqh6m1gPGiLKTmziPKmSEn1fVyjwVooQFpIYmYNAg5pLWfpeeUxyF6qxq751y-GX7mJN981dF5qk7u_QeA41JI7JBRyc/s1600/como+hacer+juegos+con+html5.png&quot;&gt;&lt;img alt=&quot;como hacer juegos con phaser&quot; loading=&quot;lazy&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2LXO5AM09tiHYIhkPpPlS0ngZhGfffYzP6jmRyGmxmIRy7Csiqh6m1gPGiLKTmziPKmSEn1fVyjwVooQFpIYmYNAg5pLWfpeeUxyF6qxq751y-GX7mJN981dF5qk7u_QeA41JI7JBRyc/s1600/como+hacer+juegos+con+html5.png&quot; title=&quot;como hacer juegos con html5&quot;/&gt;&lt;/a&gt;&lt;figcaption&gt;como hacer juegos con html5&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Las particulas en Phaser heredan propiedades y metodos de la clase &lt;b&gt;Sprite&lt;/b&gt; asi que podemos acceder a metodos del sistema de fisica directamente.&lt;/p&gt;&lt;a name=&#39;more&#39;&gt;&lt;/a&gt;&lt;p&gt;Usando los metodos propios de las particulas lo cual nos hace la tarea mas sencilla.&lt;/p&gt;&lt;p&gt;Comenzemos:&lt;/p&gt;&lt;p&gt;Creamos nuestras variables y una nueva instancia de &lt;b&gt;Phaser.Game:&lt;/b&gt;&lt;/p&gt;&lt;div class=&quot;jh-code-block&quot;&gt;&lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;span&gt;Código histórico&lt;/span&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;var game =new Phaser.Game(500, 500, Phaser.AUTO, &#39;caja&#39;, {&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&quot;jh-code-block&quot;&gt;&lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;span&gt;Código histórico&lt;/span&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;preload: preload, create: create, update: update }),&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&quot;jh-code-block&quot;&gt;&lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;span&gt;Código histórico&lt;/span&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;particulaDer, particulaIzq;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Cargamos las imagenes a utilizar en nuestra&lt;b&gt; funcion Preload&lt;/b&gt;:&lt;/p&gt;&lt;div class=&quot;jh-code-block&quot;&gt;&lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;span&gt;Código histórico&lt;/span&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;function preload() {
    game.load.spritesheet(&#39;diamante&#39;, &#39;diamantes.png&#39;, 64, 64);
};&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;En la &lt;b&gt;funcion Create &lt;/b&gt;tenemos:&lt;/p&gt;&lt;div class=&quot;jh-code-block&quot;&gt;&lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;span&gt;Código histórico&lt;/span&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;function create() {
    particulasIzq = game.add.emitter(50, game.world.centerY - 200);
   particulasIzq.bounce.setTo(0.5, 0.5);
    particulasIzq.setXSpeed(100, 200);
    particulasIzq.setYSpeed(-50, 50);
    particulasIzq.makeParticles(&#39;diamante&#39;, 0, 200, 10, true);

    particulasDer = game.add.emitter(game.world.width - 50, game.world.centerY - 200);
    particulasDer.bounce.setTo(0.5, 0.5);
    particulasDer.setXSpeed(-100, -200);
    particulasDer.setYSpeed(-50, 50);
    particulasDer.makeParticles(&#39;diamante&#39;, 1, 200, 10, true);

    // explode, lifespan, frequency, quantity
    particulasIzq.start(false, 5000,10);
    particulasDer.start(false, 5000, 10);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;En el tutorial &lt;a href=&quot;http://juegosenhtml5.blogspot.com/2014/05/crear-particulas-con-phaser.html&quot;&gt;como crear particulas&lt;/a&gt; vimos algunos de estos metodos necesarios para  agregar particulas, aqui agregamos las particulas iniciales con game.add.emitter, queremos que reboten  pero no mucho asi que usamos &lt;b&gt;bounce.setTo(0.5,0.5); &lt;/b&gt;(al rebote le coloca el 50%).&lt;/p&gt;&lt;p&gt;Para dar velocidad en sus ejes usamos&lt;b&gt; &lt;/b&gt;el metodo&lt;b&gt; setXSpeed(x,y),&lt;/b&gt;&lt;b&gt;SetYSpeed(x,y)&lt;/b&gt; ,y para crear las particulas usamos el metodo &lt;b&gt;makeParticles(key,frame,cantidad,colisiona con el mundo?).&lt;/b&gt;&lt;/p&gt;&lt;p&gt;Para terminar damos la orden que comienzen a generarse las particulas con el metodo &lt;b&gt;start(explosion?,vida de las particulas ,cantidad). &lt;/b&gt;&lt;/p&gt;&lt;p&gt;En la &lt;b&gt;funcion update&lt;/b&gt; generamos la reaccion:&lt;/p&gt;&lt;div class=&quot;jh-code-block&quot;&gt;&lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;span&gt;Código histórico&lt;/span&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;function update() {
    game.physics.arcade.collide(particulasIzq, particulasDer, cambio, null, this);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Aqui hacemos que las particulas choquen entre ellas y de ser asi llamamos a la&lt;b&gt; funcion cambio&lt;/b&gt; donde cambiaremos de color las particulas aunque tambien podriamos modificar velocidad,angulos entre otras propiedades.&lt;/p&gt;&lt;p&gt;La &lt;b&gt;funcion cambio&lt;/b&gt; quedaria asi:&lt;/p&gt;&lt;div class=&quot;jh-code-block&quot;&gt;&lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;span&gt;Código histórico&lt;/span&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;function cambio(a, b) {
    a.frame = 3;
    b.frame = 3;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;En esta funcion recibimos como parametros los objetos que colisionan como esta imagen que cargamos es un &lt;a href=&quot;http://es.wikipedia.org/wiki/Sprite_%28videojuegos%29&quot;&gt;spritesheet &lt;/a&gt;en el cual tenemos varios objetos(imagenes) con diferentes colores en diferentes partes(frames) entonces solo es cuestion de modificar el valor de dicha frame para asi acceder a otro objeto en nuestra imagen.&lt;/p&gt;&lt;p&gt;Asi conseguimos cada vez que colisionen cambien el color pero solo cuando colisionan con otra particula mas no con el cuadro(canvas) que lo rodea ademas cada que colisionan salen disparadas en direcciones contrarias tal como deberia ser una colision.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Resultado:&lt;/b&gt;&lt;/p&gt;&lt;div class=&quot;jh-demo-status&quot;&gt;&lt;div class=&quot;jh-demo-status-icon&quot;&gt;!&lt;/div&gt;&lt;div&gt;&lt;strong&gt;Demo histórico no disponible&lt;/strong&gt;&lt;p&gt;El ejemplo original estaba alojado en Google Drive Web Hosting, un servicio retirado. Conservamos esta referencia para documentar el tutorial: &lt;code&gt;https://5fd29cbe0d84bca10655114522028570a2400bbb.googledrive.com/host/0B9AohO0VxFJLell0VnlMMHhQUGs/particulascolision.html &lt;/code&gt;.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;b&gt;Hasta la proxima!!! &lt;/b&gt;&lt;/p&gt;&lt;div class=&quot;jh-article-current&quot;&gt;&lt;strong&gt;Contexto actual&lt;/strong&gt;&lt;p&gt;El código de esta entrada pertenece a una versión antigua de Phaser. El concepto de sistema de partículas, colisiones y física, sprites y recursos sigue siendo útil, pero para un proyecto nuevo conviene consultar la API de la versión de Phaser que estés utilizando en lugar de copiar literalmente estas llamadas.&lt;/p&gt;&lt;/div&gt;</description><link>http://juegosenhtml5.blogspot.com/2014/12/como-colisionar-particulas-en-phaser.html</link><author>noreply@blogger.com (a)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2LXO5AM09tiHYIhkPpPlS0ngZhGfffYzP6jmRyGmxmIRy7Csiqh6m1gPGiLKTmziPKmSEn1fVyjwVooQFpIYmYNAg5pLWfpeeUxyF6qxq751y-GX7mJN981dF5qk7u_QeA41JI7JBRyc/s72-c/como+hacer+juegos+con+html5.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7685167172351671465.post-7094468222012186964</guid><pubDate>Mon, 24 Nov 2014 18:43:00 +0000</pubDate><atom:updated>2026-08-24T15:05:18.089-05:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">como hacer jeugos</category><category domain="http://www.blogger.com/atom/ns#">fisica en phaser</category><category domain="http://www.blogger.com/atom/ns#">HTML5</category><category domain="http://www.blogger.com/atom/ns#">juegos con html5</category><category domain="http://www.blogger.com/atom/ns#">phaser</category><category domain="http://www.blogger.com/atom/ns#">Tutoriales</category><title>Cómo crear colisiones y rebotes en Phaser</title><description>&lt;p class=&quot;jh-article-lead&quot;&gt;Despues de una temporada muy enfermo vamos a ver un poco mas el uso de la fisica en &lt;b&gt;Phaser&lt;/b&gt; en el tutorial &lt;a href=&quot;http://juegosenhtml5.blogspot.com/2014/07/fisica-en-phaser.html&quot;&gt;fisica&lt;/a&gt;  hize una explicacion sobre como funciona el sistema de fisicas de este framework, ahora veamos cosas divertidas y utiles.&lt;/p&gt;&lt;div class=&quot;jh-article-note&quot;&gt;&lt;div class=&quot;jh-article-note-icon&quot;&gt;↻&lt;/div&gt;&lt;div&gt;&lt;strong&gt;Artículo original de 2014 · revisado en 2026&lt;/strong&gt;&lt;p&gt;Esta entrada forma parte del archivo histórico del blog. Se ha limpiado su HTML y conservado el contenido original; cuando utiliza APIs antiguas, se indica el contexto para no confundirlas con las versiones modernas.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;jh-version-badge is-legacy&quot;&gt;Phaser clásico · código histórico&lt;/div&gt;&lt;figure class=&quot;jh-article-figure&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgdTPVVEeChiKSlwYoPEl3xWajpznfRlnGr9gbugezX4gUOHozyNMPafd2sHp5IScGkKZvhCGDLM7pktvgEWxCJ1VRVlPmR5qZjiyWPZZz5pDIZ123ntUHsbSZYuZzokwAO8XYj5DNX1JM/s1600/basketball-30111_640.png&quot;&gt;&lt;img alt=&quot;crear colisiones con Phaser&quot; loading=&quot;lazy&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgdTPVVEeChiKSlwYoPEl3xWajpznfRlnGr9gbugezX4gUOHozyNMPafd2sHp5IScGkKZvhCGDLM7pktvgEWxCJ1VRVlPmR5qZjiyWPZZz5pDIZ123ntUHsbSZYuZzokwAO8XYj5DNX1JM/s1600/basketball-30111_640.png&quot; title=&quot;como hacer colisiones con html5&quot;/&gt;&lt;/a&gt;&lt;figcaption&gt;como hacer colisiones con html5&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Lo que vamos a hacer es accelerar una pelota en todas las direcciones y que esta colisione con todos los bordes y con cualquier otro objeto asi de simple.&lt;/p&gt;&lt;a name=&#39;more&#39;&gt;&lt;/a&gt;&lt;p&gt;Comenzemos:&lt;/p&gt;&lt;p&gt;Declaramos las variables e creamos la nueva instancia de &lt;b&gt;Phaser.Game&lt;/b&gt;:&lt;/p&gt;&lt;div class=&quot;jh-code-block&quot;&gt;&lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;span&gt;Código histórico&lt;/span&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;var juego,balon,muñeco, teclas;

var game = new Phaser.Game(500, 500, Phaser.CANVAS, &#39;caja&#39;,
 { preload: preload, create: create, update: update });&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Creamos nuestra funcion create:&lt;/p&gt;&lt;div class=&quot;jh-code-block&quot;&gt;&lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;span&gt;Código histórico&lt;/span&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;function create() {
    game.physics.startSystem(Phaser.Physics.ARCADE);
  teclas = game.input.keyboard.createCursorKeys();
    balon = game.add.sprite(400, 200, &#39;ball&#39;);
    muñeco = game.add.sprite(400, 200, &#39;dude&#39;);
    game.physics.enable([balon,muñeco], Phaser.Physics.ARCADE);
    muñeco.body.immovable = true;
    balon.body.velocity.setTo(200, 200);
    balon.body.collideWorldBounds = true;
    balon.body.bounce.setTo(1, 1);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bueno inicializamos el sistema de fisica, creamos nuestras teclas, agregamos los sprites, activamos la fisica a ambos objetos.&lt;/p&gt;&lt;p&gt;No queremos que cuando la bola y el muñeco choquen, el muñeco salga disparado tambien entonces le decimos que no se mueva esto es que &lt;b&gt;immovable&lt;/b&gt; sea verdadero, al balon le agregamos velocidad en sus ambos ejes, le permitimos colisionar con el mundo nuestro canvas y con la propidad &lt;b&gt;bounce&lt;/b&gt; lo hacemos rebotar cada que colisione.&lt;/p&gt;&lt;p&gt;Esta propiedad no es mas que un punto(x,y) que es multiplicado a la velocidad, facil un ejemplo seria:  &lt;b&gt;velocidadBalon.x *= -fuerzarebote; &lt;/b&gt;Con colocarle 1 le estamos colocando el maximo valor seria como el 100% de fuerza al rebotar esto quiere decir que 0.5 seria 50%.&lt;/p&gt;&lt;p&gt;En la funcion update tenemos:&lt;/p&gt;&lt;div class=&quot;jh-code-block&quot;&gt;&lt;div class=&quot;jh-code-head&quot;&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;span&gt;Código histórico&lt;/span&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;function update () {
    game.physics.arcade.collide(muñeco, balon);
    if (teclas.up.isDown)
    {
        muñeco.body.velocity.y = -300;
    }
    else if (teclas.down.isDown)
    {
       muñeco.body.velocity.y =  300;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Aqui hacemos posible que los dos objetos (muñeco, balon) colisionen y que este muñeco se pueda mover en en su eje Y.
Asi de manera simple tenemos algo llamativo, con el cual podemos usar para hacer el clasico pin-pong o otros juegos divertidos.&lt;/p&gt;&lt;div class=&quot;jh-demo-status&quot;&gt;&lt;div class=&quot;jh-demo-status-icon&quot;&gt;!&lt;/div&gt;&lt;div&gt;&lt;strong&gt;Demo histórico no disponible&lt;/strong&gt;&lt;p&gt;El ejemplo original estaba alojado en Google Drive Web Hosting, un servicio retirado. Conservamos esta referencia para documentar el tutorial: &lt;code&gt;https://12c4452b5cb31a16182543570478b168fcdade85.googledrive.com/host/0B9AohO0VxFJLeEp2R2xrLTVQZVk/rebote.html &lt;/code&gt;.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Hasta la proxima !!!.&lt;/p&gt;&lt;div class=&quot;jh-article-current&quot;&gt;&lt;strong&gt;Contexto actual&lt;/strong&gt;&lt;p&gt;El código de esta entrada pertenece a una versión antigua de Phaser. El concepto de colisiones y física, sprites y recursos sigue siendo útil, pero para un proyecto nuevo conviene consultar la API de la versión de Phaser que estés utilizando en lugar de copiar literalmente estas llamadas.&lt;/p&gt;&lt;/div&gt;</description><link>http://juegosenhtml5.blogspot.com/2014/11/como-hacer-colisiones.html</link><author>noreply@blogger.com (a)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgdTPVVEeChiKSlwYoPEl3xWajpznfRlnGr9gbugezX4gUOHozyNMPafd2sHp5IScGkKZvhCGDLM7pktvgEWxCJ1VRVlPmR5qZjiyWPZZz5pDIZ123ntUHsbSZYuZzokwAO8XYj5DNX1JM/s72-c/basketball-30111_640.png" height="72" width="72"/><thr:total>0</thr:total></item></channel></rss>