Skip to main content

Wheel of Fortune HTML5 Canvas Game

This demo shows how to create an interactive Wheel of Fortune game using Konva. The wheel can be spun with mouse or touch input, and it will gradually slow down due to angular friction. When it stops, it will show your prize!

import Konva from 'konva';

Konva.angleDeg = false;
let angularVelocity = 6;
const angularVelocities = [];
let lastRotation = 0;
let controlled = false;
const numWedges = 25;
const angularFriction = 0.2;
let target, activeWedge, stage, layer, wheel, pointer;
let finished = false;

function getAverageAngularVelocity() {
  const total = angularVelocities.reduce((sum, vel) => sum + vel, 0);
  return angularVelocities.length ? total / angularVelocities.length : 0;
}

function purifyColor(color) {
  const randIndex = Math.round(Math.random() * 3);
  color[randIndex] = 0;
  return color;
}

function getRandomColor() {
  const r = 100 + Math.round(Math.random() * 55);
  const g = 100 + Math.round(Math.random() * 55);
  const b = 100 + Math.round(Math.random() * 55);
  return purifyColor([r, g, b]);
}

function getRandomReward() {
  const mainDigit = Math.round(Math.random() * 9);
  return mainDigit + '\n0\n0';
}

function addWedge(n) {
  const s = getRandomColor();
  const reward = getRandomReward();
  const [r, g, b] = s;
  const angle = (2 * Math.PI) / numWedges;

  const endColor = `rgb(${r},${g},${b})`;
  const startColor = `rgb(${r + 100},${g + 100},${b + 100})`;

  const wedge = new Konva.Group({
    rotation: (2 * n * Math.PI) / numWedges,
  });

  const wedgeBackground = new Konva.Wedge({
    radius: 400,
    angle: angle,
    fillRadialGradientStartPoint: 0,
    fillRadialGradientStartRadius: 0,
    fillRadialGradientEndPoint: 0,
    fillRadialGradientEndRadius: 400,
    fillRadialGradientColorStops: [0, startColor, 1, endColor],
    fill: '#64e9f8',
    fillPriority: 'radial-gradient',
    stroke: '#ccc',
    strokeWidth: 2,
  });

  wedge.add(wedgeBackground);

  const text = new Konva.Text({
    text: reward,
    fontFamily: 'Calibri',
    fontSize: 50,
    fill: 'white',
    align: 'center',
    stroke: 'yellow',
    strokeWidth: 1,
    rotation: (Math.PI + angle) / 2,
    x: 380,
    y: 30,
    listening: false,
  });

  wedge.add(text);
  text.cache();

  wedge.startRotation = wedge.rotation();
  wheel.add(wedge);
}

function animate(frame) {
  // handle wheel spin
  const angularVelocityChange =
    (angularVelocity * frame.timeDiff * (1 - angularFriction)) / 1000;
  angularVelocity -= angularVelocityChange;

  // activate / deactivate wedges based on point intersection
  const shape = stage.getIntersection({
    x: stage.width() / 2,
    y: 100,
  });

  if (controlled) {
    if (angularVelocities.length > 10) {
      angularVelocities.shift();
    }

    angularVelocities.push(
      ((wheel.rotation() - lastRotation) * 1000) / frame.timeDiff
    );
  } else {
    const diff = (frame.timeDiff * angularVelocity) / 1000;
    if (diff > 0.0001) {
      wheel.rotate(diff);
    } else if (!finished && !controlled) {
      if (shape) {
        const text = shape.getParent().findOne('Text').text();
        const price = text.split('\n').join('');
        alert('Your price is ' + price);
      }
      finished = true;
    }
  }
  lastRotation = wheel.rotation();

  if (shape && (!activeWedge || shape._id !== activeWedge._id)) {
    pointer.y(20);

    new Konva.Tween({
      node: pointer,
      duration: 0.3,
      y: 30,
      easing: Konva.Easings.ElasticEaseOut,
    }).play();

    if (activeWedge) {
      activeWedge.fillPriority('radial-gradient');
    }
    shape.fillPriority('fill');
    activeWedge = shape;
  }
}

stage = new Konva.Stage({
  container: 'container',
  width: window.innerWidth,
  height: 400,
});

layer = new Konva.Layer();
wheel = new Konva.Group({
  x: stage.width() / 2,
  y: 410,
});

for (let n = 0; n < numWedges; n++) {
  addWedge(n);
}

pointer = new Konva.Wedge({
  fillRadialGradientStartPoint: 0,
  fillRadialGradientStartRadius: 0,
  fillRadialGradientEndPoint: 0,
  fillRadialGradientEndRadius: 30,
  fillRadialGradientColorStops: [0, 'white', 1, 'red'],
  stroke: 'white',
  strokeWidth: 2,
  lineJoin: 'round',
  angle: 1,
  radius: 30,
  x: stage.width() / 2,
  y: 33,
  rotation: -90,
  shadowColor: 'black',
  shadowOffsetX: 3,
  shadowOffsetY: 3,
  shadowBlur: 2,
  shadowOpacity: 0.5,
});

// add components to the stage
layer.add(wheel);
layer.add(pointer);
stage.add(layer);

// bind events
wheel.on('mousedown touchstart', function (evt) {
  angularVelocity = 0;
  controlled = true;
  target = evt.target;
  finished = false;
});

stage.on('mouseup touchend', function () {
  controlled = false;
  angularVelocity = getAverageAngularVelocity() * 5;

  if (angularVelocity > 20) {
    angularVelocity = 20;
  } else if (angularVelocity < -20) {
    angularVelocity = -20;
  }

  angularVelocities.length = 0;
});

stage.on('mousemove touchmove', function () {
  const mousePos = stage.getPointerPosition();
  if (controlled && mousePos && target) {
    const x = mousePos.x - wheel.getX();
    const y = mousePos.y - wheel.getY();
    const atan = Math.atan(y / x);
    const rotation = x >= 0 ? atan : atan + Math.PI;

    wheel.rotation(rotation);
  }
});

// create animation
const anim = new Konva.Animation(animate, layer);
anim.start();