Skip to main content

Slots

import * as PIXI from 'pixi.js';

const app = new PIXI.Application({ background: '#1099bb', resizeTo: window });

document.body.appendChild(app.view);

PIXI.Assets.load([
'https://pixijs.com/assets/eggHead.png',
'https://pixijs.com/assets/flowerTop.png',
'https://pixijs.com/assets/helmlok.png',
'https://pixijs.com/assets/skully.png',
]).then(onAssetsLoaded);

const REEL_WIDTH = 160;
const SYMBOL_SIZE = 150;

// onAssetsLoaded handler builds the example.
function onAssetsLoaded()
{
// Create different slot symbols.
const slotTextures = [
PIXI.Texture.from('https://pixijs.com/assets/eggHead.png'),
PIXI.Texture.from('https://pixijs.com/assets/flowerTop.png'),
PIXI.Texture.from('https://pixijs.com/assets/helmlok.png'),
PIXI.Texture.from('https://pixijs.com/assets/skully.png'),
];

// Build the reels
const reels = [];
const reelContainer = new PIXI.Container();

for (let i = 0; i < 5; i++)
{
const rc = new PIXI.Container();

rc.x = i * REEL_WIDTH;
reelContainer.addChild(rc);

const reel = {
container: rc,
symbols: [],
position: 0,
previousPosition: 0,
blur: new PIXI.filters.BlurFilter(),
};

reel.blur.blurX = 0;
reel.blur.blurY = 0;
rc.filters = [reel.blur];

// Build the symbols
for (let j = 0; j < 4; j++)
{
const symbol = new PIXI.Sprite(slotTextures[Math.floor(Math.random() * slotTextures.length)]);
// Scale the symbol to fit symbol area.

symbol.y = j * SYMBOL_SIZE;
symbol.scale.x = symbol.scale.y = Math.min(SYMBOL_SIZE / symbol.width, SYMBOL_SIZE / symbol.height);
symbol.x = Math.round((SYMBOL_SIZE - symbol.width) / 2);
reel.symbols.push(symbol);
rc.addChild(symbol);
}
reels.push(reel);
}
app.stage.addChild(reelContainer);

// Build top & bottom covers and position reelContainer
const margin = (app.screen.height - SYMBOL_SIZE * 3) / 2;

reelContainer.y = margin;
reelContainer.x = Math.round(app.screen.width - REEL_WIDTH * 5);
const top = new PIXI.Graphics();

top.beginFill(0, 1);
top.drawRect(0, 0, app.screen.width, margin);
const bottom = new PIXI.Graphics();

bottom.beginFill(0, 1);
bottom.drawRect(0, SYMBOL_SIZE * 3 + margin, app.screen.width, margin);

// Add play text
const style = new PIXI.TextStyle({
fontFamily: 'Arial',
fontSize: 36,
fontStyle: 'italic',
fontWeight: 'bold',
fill: ['#ffffff', '#00ff99'], // gradient
stroke: '#4a1850',
strokeThickness: 5,
dropShadow: true,
dropShadowColor: '#000000',
dropShadowBlur: 4,
dropShadowAngle: Math.PI / 6,
dropShadowDistance: 6,
wordWrap: true,
wordWrapWidth: 440,
});

const playText = new PIXI.Text('Spin the wheels!', style);

playText.x = Math.round((bottom.width - playText.width) / 2);
playText.y = app.screen.height - margin + Math.round((margin - playText.height) / 2);
bottom.addChild(playText);

// Add header text
const headerText = new PIXI.Text('PIXI MONSTER SLOTS!', style);

headerText.x = Math.round((top.width - headerText.width) / 2);
headerText.y = Math.round((margin - headerText.height) / 2);
top.addChild(headerText);

app.stage.addChild(top);
app.stage.addChild(bottom);

// Set the interactivity.
bottom.eventMode = 'static';
bottom.cursor = 'pointer';
bottom.addListener('pointerdown', () =>
{
startPlay();
});

let running = false;

// Function to start playing.
function startPlay()
{
if (running) return;
running = true;

for (let i = 0; i < reels.length; i++)
{
const r = reels[i];
const extra = Math.floor(Math.random() * 3);
const target = r.position + 10 + i * 5 + extra;
const time = 2500 + i * 600 + extra * 600;

tweenTo(r, 'position', target, time, backout(0.5), null, i === reels.length - 1 ? reelsComplete : null);
}
}

// Reels done handler.
function reelsComplete()
{
running = false;
}

// Listen for animate update.
app.ticker.add((delta) =>
{
// Update the slots.
for (let i = 0; i < reels.length; i++)
{
const r = reels[i];
// Update blur filter y amount based on speed.
// This would be better if calculated with time in mind also. Now blur depends on frame rate.

r.blur.blurY = (r.position - r.previousPosition) * 8;
r.previousPosition = r.position;

// Update symbol positions on reel.
for (let j = 0; j < r.symbols.length; j++)
{
const s = r.symbols[j];
const prevy = s.y;

s.y = ((r.position + j) % r.symbols.length) * SYMBOL_SIZE - SYMBOL_SIZE;
if (s.y < 0 && prevy > SYMBOL_SIZE)
{
// Detect going over and swap a texture.
// This should in proper product be determined from some logical reel.
s.texture = slotTextures[Math.floor(Math.random() * slotTextures.length)];
s.scale.x = s.scale.y = Math.min(SYMBOL_SIZE / s.texture.width, SYMBOL_SIZE / s.texture.height);
s.x = Math.round((SYMBOL_SIZE - s.width) / 2);
}
}
}
});
}

// Very simple tweening utility function. This should be replaced with a proper tweening library in a real product.
const tweening = [];

function tweenTo(object, property, target, time, easing, onchange, oncomplete)
{
const tween = {
object,
property,
propertyBeginValue: object[property],
target,
easing,
time,
change: onchange,
complete: oncomplete,
start: Date.now(),
};

tweening.push(tween);

return tween;
}
// Listen for animate update.
app.ticker.add((delta) =>
{
const now = Date.now();
const remove = [];

for (let i = 0; i < tweening.length; i++)
{
const t = tweening[i];
const phase = Math.min(1, (now - t.start) / t.time);

t.object[t.property] = lerp(t.propertyBeginValue, t.target, t.easing(phase));
if (t.change) t.change(t);
if (phase === 1)
{
t.object[t.property] = t.target;
if (t.complete) t.complete(t);
remove.push(t);
}
}
for (let i = 0; i < remove.length; i++)
{
tweening.splice(tweening.indexOf(remove[i]), 1);
}
});

// Basic lerp funtion.
function lerp(a1, a2, t)
{
return a1 * (1 - t) + a2 * t;
}

// Backout function from tweenjs.
// https://github.com/CreateJS/TweenJS/blob/master/src/tweenjs/Ease.js
function backout(amount)
{
return (t) => (--t * t * ((amount + 1) * t + amount) + 1);
}