How We Built a High‑Performance H5 Coin‑Pusher Game with CreateJS & Matter.js
This article details the end‑to‑end development of a 2D H5 coin‑pusher game for JD's 11.11 promotion, covering background, technology choices, implementation of obstacles, pusher, coins, skills, debugging techniques, and performance optimizations using CreateJS and Matter.js.
Background
The annual Double‑Eleven shopping festival required a fresh H5 interactive mini‑game for JD's WeChat marketing. The team chose a coin‑pusher concept, inspired by arcade machines, to drive social sharing and coupon distribution.
Pre‑research
After testing several coin‑pusher apps on the App Store, the core gameplay was simple but few H5 implementations existed. The team initially explored a 3D solution with Three.js and Ammo.js, but time constraints, limited 3D experience, and performance concerns on mobile WebGL led to abandoning the 3D approach.
Technology Selection
CreateJS – widely used within the team, offering a stable rendering engine.
Matter.js – a lightweight, well‑documented 2D physics engine that met the game’s requirements.
Implementation Overview
The game runs entirely on a Canvas, with the background image covering the scene. Core elements include obstacles, a pusher, coins, prizes, and skill effects. Each element’s implementation is described below.
Obstacles
Areas outside the defined activity zone become obstacles to restrict coin movement. Matter.js’s Bodies.fromVertices creates irregular shapes from vertex coordinates, with poly‑decomp added for compatibility.
World.add(this.world, [
Bodies.fromVertices(282, 332, [
{x: 0, y: 0},
{x: 0, y: 890},
{x: 140, y: 815},
{x: 208, y: 614},
{x: 548, y: 614},
{x: 612, y: 815},
{x: 750, y: 890},
{x: 750, y: 0}
])
]);Pusher
Creation : A CreateJS Bitmap is used for the visual pusher. The physics body is a trapezoid that matches the visual shape, attached to the pusher object for easy access.
var bounds = this.pusher.getBounds();
this.pusher.body = Matter.Bodies.trapezoid(
this.pusher.x,
this.pusher.y,
bounds.width,
bounds.height
);
Matter.World.add(this.world, [this.pusher.body]);Scaling (extension/retraction) : The pusher moves forward and backward, scaling to create a pseudo‑3D effect. Direction, velocity, scaling ratio, and synchronization with the physics body are all controlled in the beforeUpdate event.
var direction, velocity, ratio, deltaY, minY = 550, maxY = 720, minScale = .74;
Matter.Events.on(this.engine, 'beforeUpdate', function(event) {
// Length control
if (this.isPusherLengthen) {
velocity = 90;
this.pusherMaxY = maxY;
} else {
velocity = 85;
this.pusherMaxY = 620;
}
// Direction control
if (this.pusher.y >= this.pusherMaxY) {
direction = -1;
this.isPusherLengthen = false;
} else if (this.pusher.y <= this.pusherMinY) {
direction = 1;
}
// Velocity control
this.pusher.y += direction * velocity;
// Scale control
ratio = (1 - minScale) * ((this.pusher.y - minY) / (maxY - minY));
this.pusher.scaleX = this.pusher.scaleY = minScale + ratio;
// Sync physics body
Body.setPosition(this.pusher.body, {x: this.pusher.x, y: this.pusher.y});
});Masking : To prevent the pusher’s extended part from overflowing the canvas, a CreateJS mask is applied.
var shape = new createjs.Shape();
shape.graphics.beginFill('#ffffff').drawRect(0, 612, 750, 220);
this.pusher.mask = shape;Coins
Coin handling is split into three stages to keep physics manageable and visual effects smooth.
Stage 1 – Coins are created at the moving spawn point, added to the stage with a random z‑index to avoid uniform stacking.
Stage 2 – Gravity is set to zero; coins remain on the platform and are moved by the pusher.
Stage 3 – When a coin leaves the pusher, a physics body is created for collision handling, and the coin eventually falls off the bottom edge.
// Random z‑index
var index = Utils.getRandomInt(1, Game.coinContainer.getNumChildren());
Game.coinContainer.setChildIndex(this.coin, index);
// Disable world gravity for coins
this.engine = Matter.Engine.create();
this.engine.world.gravity.y = 0;
// Collision handling and body creation
Matter.Events.on(this.engine, 'beforeUpdate', function(event) {
for (var i = 0; i < this.coins.length; i++) {
var coin = this.coins[i];
if (coin.sprite.y < this.pusher.y) {
// Move with pusher and apply scaling
if (deltaY > 0) coin.sprite.y += deltaY; else coin.sprite.y -= deltaY;
if (coin.sprite.scaleX < 1) {
coin.sprite.scaleX += 0.001;
coin.sprite.scaleY += 0.001;
}
} else {
if (coin.body) {
Matter.Body.set(coin.body, {position: {x: coin.sprite.x, y: coin.sprite.y}});
} else {
coin.body = Matter.Bodies.circle(coin.sprite.x, coin.sprite.y);
Matter.World.add(this.world, [coin.body]);
}
}
}
});Prizes
Prizes are kept separate from coins to avoid collision handling, which caused a “crab‑walk” effect; therefore they are moved independently.
Skill Design
Shake : A CSS3 shake animation (using the csshake library) is applied to the stage container. On Android devices, the Vibration API adds a tactile effect.
if (isAndroid) {
window.navigator.vibrate = navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate;
window.navigator.vibrate([100,30,100,30,100,200,200,30,200,30,200,200,100,30,100,30,100]);
window.navigator.vibrate(0);
}Extension : Extending the pusher changes its maximum Y coordinate while preserving movement speed to avoid sudden jumps.
Debugging Techniques
A transparent Matter.js render layer is overlaid on the CreateJS scene to visualize physics bodies.
Matter.Render.create({
element: document.getElementById('debugger-canvas'),
engine: this.engine,
options: {
width: 750,
height: 1206,
showVelocity: true,
wireframes: false
}
});
this.pusher.body = Matter.Bodies.trapezoid(..., {
isStatic: true,
render: {opacity: .5, fillStyle: 'red'}
});Performance / Experience Optimizations
Control Object Count
Increasing coin count raises collision calculations, causing lag. Reducing each coin’s physics radius distributes them more evenly and limits the total number of active bodies.
Android Stutter
Fixed‑speed pusher movement felt choppy on low‑FPS Android devices. The movement is now time‑delta based, ensuring consistent speed across frame rates.
var delta = 0, prevTime = 0;
Matter.Events.on(this.engine, 'beforeUpdate', function(event) {
delta = event.timestamp - prevTime;
prevTime = event.timestamp;
this.pusher.y += direction * velocity * (delta / 1000);
});Object Recycling
Objects that leave the visible area are recycled instead of destroyed, reducing memory churn.
Event Cleanup
createjs.Tween.removeTweens(this.coin);Conclusion
The article walks through the complete development pipeline of the coin‑pusher mini‑game, from initial concept to final performance tweaks, providing concrete code snippets and practical lessons for building responsive H5 games.
Related Resources
Three.js official site
Three.js Getting Started Guide
Three.js “Learn While Building” tutorial
Matter.js official site
Matter.js 2D Physics Engine Review
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Aotu Lab
Aotu Lab, founded in October 2015, is a front-end engineering team serving multi-platform products. The articles in this public account are intended to share and discuss technology, reflecting only the personal views of Aotu Lab members and not the official stance of JD.com Technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
