Click here to Skip to main content
15,868,016 members
Articles / Artificial Intelligence
Article

How the "Be the Thief" experience was created

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
5 Apr 2013CPOL10 min read 18.2K   5   1
How the "Be the Thief" experience was created

This article is for our sponsors at CodeProject. These articles are intended to provide you with information on products and services that we consider useful and of value to developers

Develop a Windows 8 app in 30 days

Intro

'Thief of Thieves' is a new graphic novel series published by Skybound Comics, home of the popular series The Walking Dead. Thief of Thieves is the story of of Conrad Paulson, the world’s greatest thief, who in a move of redemption begins to steal from other thieves to right the wrongs of his life. AMC TV, home of The Walking Dead show, is currently developing a show based on Thief of Thieves.

IE10: Be the Thief is a collaboration between Skybound and the Internet Explorer team which publicizes the already well-received graphic novel series through a set of cutting-edge, cross-browser experiences that highlight the best of the modern web.

Our goal with the Thief of Thieves Experience is to deliver an immersive experience through Internet Explorer 10 equal to one you’d typically find in a native application. We do this by using the latest in HTML5, CSS3, touch, and related technologies.

Taking full advantage of Internet Explorer 10's hardware-accelerated SVG processing and industry-leading touch support, we place users in the comic book world of Robert Kirkman's Thief of Thieves, letting them walk in the proverbial footsteps of the series' art-thief protagonist.

Perfect for Touch

Internet Explorer raises the bar on browser support for pointer events, and we take full advantage of this for the Thief of Thieves Experience. For instance, users can spin a dial using multi-touch to practice their safe-cracking skills. And Internet Explorer’s support for touch controls using the newly submitted Pointer Events API made this remarkably easy to do, enabling us to build a great touch-based rotation experience.

Let's take a look (in both CoffeeScript and its rendered Javascript):

CoffeeScript

@wheel = $('#Dial')
@angle = 0
 
if window.MSGesture
    gesture = new MSGesture()
    gesture.target = @wheel[0]
    @wheel.bind("MSGestureChange", @didRotate).bind("MSPointerDown", @addGesturePointer)
else
    @wheel.mousedown(@startRotate)
    $(window).mousemove(@didRotate).mouseup(@stopRotate)
 
addGesturePointer:(e)=>
    #Handle Touch v. Mouse in IE10
    if e.originalEvent.pointerType != 2 #Not Touch Input
        e.target.msSetPointerCapture(e.originalEvent.pointerId)
        @startRotate(e.originalEvent)
        @gesture = false
        $(window).bind("MSPointerMove", @didRotate).bind("MSPointerUp", @stopRotate)
    else
    @gesture && @gesture.addPointer(e.originalEvent.pointerId)
 
startRotate: (e)=>
    document.onselectstart = (e)-> false
    @rotating = true
    currentPoint = screenPointToSvg(e.clientX, e.clientY, @wheel[0])
    angle = Math.atan2(currentPoint[1] - @center_y + 55, currentPoint[0] - @center_x + 90)
    angle = angle * LockpickGame.rad2degree
    @mouseStartAngle = angle
    @dialStartAngle = @angle
 
didRotate: (e)=>
    return unless @rotating
    if @gesture
        @angle += e.originalEvent.rotation * LockpickGame.rad2degree
    else
    if e.originalEvent.pointerType
        e = e.originalEvent
    currentPoint = screenPointToSvg(e.clientX, e.clientY, @wheel[0])
    angle = Math.atan2(currentPoint[1] - @center_y - 20, currentPoint[0] - @center_x + 0)
    angle = angle * LockpickGame.rad2degree
    @angle = @normalizeAngle(@dialStartAngle)+(@normalizeAngle(angle) - @normalizeAngle(@mouseStartAngle))
 
    @center_x = 374.3249938
    @center_y = 354.7909851
    rotate_transform = "rotate(#{@angle} #{@center_x} #{@center_y})"
    requestAnimationFrame(()=>
        @wheel.attr("transform", rotate_transform)
    )
 
stopRotate: (e)=>
    document.onselectstart = (e)-> true
    @rotating = false

JavaScript

var gesture,
    _this = this;
 
this.wheel = $('#Dial');
 
this.angle = 0;
 
if (window.MSGesture) {
    gesture = new MSGesture();
    gesture.target = this.wheel[0];
    this.wheel.bind("MSGestureChange", this.didRotate).bind("MSPointerDown", this.addGesturePointer);
} else {
    this.wheel.mousedown(this.startRotate);
    $(window).mousemove(this.didRotate).mouseup(this.stopRotate);
}
 
({
    addGesturePointer: function(e) {
        if (e.originalEvent.pointerType !== 2) {
            e.target.msSetPointerCapture(e.originalEvent.pointerId);
            _this.startRotate(e.originalEvent);
            _this.gesture = false;
            return $(window).bind("MSPointerMove", _this.didRotate).bind("MSPointerUp", _this.stopRotate);
        } else {
            return _this.gesture && _this.gesture.addPointer(e.originalEvent.pointerId);
        }
    },
    startRotate: function(e) {
        var angle, currentPoint;
        document.onselectstart = function(e) {
        return false;
        };
        _this.rotating = true;
        currentPoint = screenPointToSvg(e.clientX, e.clientY, _this.wheel[0]);
        angle = Math.atan2(currentPoint[1] - _this.center_y + 55, currentPoint[0] - _this.center_x + 90);
        angle = angle * LockpickGame.rad2degree;
        _this.mouseStartAngle = angle;
        return _this.dialStartAngle = _this.angle;
    },
    didRotate: function(e) {
        var angle, currentPoint, rotate_transform;
        if (!_this.rotating) {
            return;
        }
        if (_this.gesture) {
            _this.angle += e.originalEvent.rotation * LockpickGame.rad2degree;
        } else {
        if (e.originalEvent.pointerType) {
            e = e.originalEvent;
        }
        currentPoint = screenPointToSvg(e.clientX, e.clientY, _this.wheel[0]);
        angle = Math.atan2(currentPoint[1] - _this.center_y - 20, currentPoint[0] - _this.center_x + 0);
        angle = angle * LockpickGame.rad2degree;
        _this.angle = _this.normalizeAngle(_this.dialStartAngle) + (_this.normalizeAngle(angle) - _this.normalizeAngle(_this.mouseStartAngle));
    }
    _this.center_x = 374.3249938;
    _this.center_y = 354.7909851;
    rotate_transform = "rotate(" + _this.angle + " " + _this.center_x + " " + _this.center_y + ")";
    return requestAnimationFrame(function() {
        return _this.wheel.attr("transform", rotate_transform);
    });
    },
    stopRotate: function(e) {
        document.onselectstart = function(e) {
            return true;
    };
    return _this.rotating = false;
    }
});

Essentially, we bind the MSGestureChange event to our didRotate function. If a player is using touch rather than a mouse, we need only three lines to calculate and move the dial, rather than eight for a traditional mouse pointer. IE takes care of the rest, including inertial motion. The Gesture API returns both an angle and an inertia value to make hooking everything together a snap.

Touch pervades almost all of the experience, from drawing the player’s character to pickpocketing and navigating the end-game Heist. Combined with Internet Explorer’s chrome-less, immersive, full-screen mode in Windows 8, it serves to draw the user more fully into the action, much like a native application.

The Joys of Vector

Computing, and the web in particular, is moving toward a resolution-independent world. Your website or application needs to look good on screens ranging from low-res to high; vector graphics, and SVG especially, become a key tool in a web developer's toolbox.

SVG allows you to express images in code directly, which allows the image to scale infinitely without any loss of quality or fidelity. And with a new ability in Internet Explorer, you can apply filters to your SVG, making it easy to programmatically add complicated effects like drop shadows and blurs.

The code below is the SVG code for the star image to the right. The <polygon> element draws the actual star shape, and then setting the filter attribute on it to the id of a <filter> element is what causes the filter to actually show up.

XML
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
    <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="227px" height="197px" viewBox="0 0 227 197" enable-background="new 0 0 227 197" xml:space="preserve">
    <defs> <!-- Placed just after your opening svg element -->
        <filter width="200%" height="200%" x="0" y="0" id="blurrydropshadow">
            <feOffset in="SourceAlpha" result="offOut" dy="10" dx="10"></feOffset>
            <feGaussianBlur in="offOut" result="blurOut" stdDeviation="7"></feGaussianBlur>
            <feBlend in="SourceGraphic" in2="blurOut" mode="normal"></feBlend>
        </filter>
        <filter width="200%" height="200%" x="0" y="0" id="soliddropshadow">
            <feOffset in="SourceAlpha" result="offOut" dy="10" dx="10"></feOffset>
            <feBlend in="SourceGraphic" in2="offOut" mode="normal"></feBlend>
        </filter>
    </defs>
    <polygon fill="#4C5BA9" filter="url(#soliddropshadow)" stroke="#000000" stroke-width="4" stroke-miterlimit="10" points="113.161,24.486 135.907,70.576
186.77,77.966 149.965,113.843 158.654,164.5 113.161,140.583 67.667,164.5 76.356,113.843 39.552,77.966 90.414,70.576 "/>
</svg>

We also found that Internet Explorer performed better in terms of speed and smoothness in applying animation to html, including top-level SVG elements, than other browsers in our arsenal, largely due to the hardware-acceleration that Internet Explorer provides to all html elements.

Images as code

One of the great things about using SVG is that they’re easy to move around, edit, and save, since they’re natively represented as text and can be modified on the fly with JavaScript. This is used to great effect in the Alias Creator, where we pull in external SVG for each of the possible characters, as well as all of the available facial features and accessories. We then display all of that together as an editable drawing surface, allowing the user to add features and accessories as well as use their cursor or finger to draw directly on the screen.

Once the user is done creating their alias, the SVG data gets saved using HTML5’s localStorage API. At that point the user’s alias can be used anywhere across the site. The alias is simply pulled from local storage and used as any other image would be (except that this one can be used at any size), without the need for any image encoding, etc. The other great feature to this is that when the user wants to edit their alias, it’s just as easy: Load in the already created character, load the editor, and create!

Optimization

An important thing to consider when using SVG for the web, is that there is a great deal of optimization that can be done to them after you’ve exported the artwork from your vector art application. Most programs like Adobe Illustrator and Inkscape tend to leave a lot of elements in the files that don’t need to be there, and leave very complicated paths that can be simplified without losing any image definition. This site was one of our first large SVG undertakings and we learned a lot while figuring out a proper SVG workflow. As we were using a mix of inline SVG, and SVG as <img> tags, we needed to take a few different approaches to optimization.

As a general rule, for any of the artwork that wasn’t going to need internal animation, we ran it all through one of the various pieces of SVG Cleaning software available such as CleanSVG or Scour. For some of the inline SVG, we needed to take a much more meticulous approach, removing extra groups and doing path simplification within the vector editor itself to make sure we didn’t lose ID attributes needed for scripting.

Front-End Tech

As we alluded above, much of this project was written in a combination of CoffeeScript and regular JavaScript.

As you saw in the code snippet earlier, CoffeeScript allows us to write JavaScript in a much more concise manner, but before it's live in your browser, everything compiles down to the equivalent JavaScript.

We tried CoffeeScript as an experiment in emerging web technology that can marry well with web standards-based development, because at the end of the day, (to quote the CoffeeScript docs), it's just JavaScript.

On the CSS side, we also took Less for a spin. Similar to CoffeeScript, Less is a preprocessor for style sheets that enabled us to build more flexible and maintainable CSS. Its additions of variables, mixins, and operations proved invaluable.

 

Bag of Tricks

In "The Heist," our thief has to move through levels without being detected. In the later levels, there are patrolling guards that need to be distracted by the thief (with a simple double-tap somewhere on the level map) so that the player can exit the level safely. We needed an algorithm to map a path around the level for the guard to follow and investigate the distraction event fired by the thief.

A Star (A*) – A Pathing Algorithm

A Star, also written as A*, is the most common path finding algorithm for graphs that exists today. Under certain circumstances, A* guarantees that it will always find the shortest path between two points if that path exists, provided there aren’t obstacles that make it impossible to move from point A to point B. It is commonly used in games, robotics etc.

Here’s the Wikipedia A* entry which includes its pseudo code: http://en.wikipedia.org/wiki/A_star

Why A*?

In this case it was very easy to use A* as the map was already a grid, which is simply a type of graph, and the map didn’t contain any special objects like elevators, teleporters etc. which may break A*. To cater for those types of objects we would’ve had to use another algorithm like the Djisktra algorithm.

Challenges in Implementation

In using the A* algorithm for creating paths for our AI security guards to follow, we encountered a few challenges in terms of JavaScript's support for data structures. A* implementation usually requires several data structures that just don’t exist in JavaScript. For example, it is common to represent some of its internal lists as hash tables, and while JS has dictionaries in the forms of objects they can only use strings for keys, so we needed to make sure every time we operated with them we hashed our objects.

Another issue was the need of a priority queue. For efficiency, A* needs to have a queue of nodes that it may visit ordered by the “best” candidate node. That idea is represented by a priority queue, which is often implemented as a binary heap. This data structure doesn’t exist in JS, so we implemented it based on the classic pseudo code from the book “Introduction to Algorithms.” This book is the bible for these kind of topics.

Just 200 Lines of Code

Our JavaScript implantation of the A* algorithm for Thief of Thieves is around 200 lines of code, including comments! We invite people interested in learning a little more about path finding and artificial intelligence to look at astar.js and read the Wikipedia article mentioned above.

* A* pathfinding algorithm implementation based on the the pseudo-code
* from the Wikipedia article (http://en.wikipedia.org/wiki/A_star)
* Date: 02/05/2013
*/
 
"use strict";
 
// Node of the search graph
function GraphNode(x, y) {
    this.x = x;
    this.y = y;
    this.hash = x + "" + y;
    this.f = Number.POSITIVE_INFINITY;
}
 
// Constructor for the pathfinder. It takes a grid which is an array of arrays
// indicating whether a position is passable (0) or it's blocked (1)
function AStar(grid) {
    // Compares to nodes by their f value
    function nodeComparer(left, right) {
        if (left.f > right.f) {
            return 1;
        }
 
        if (left.f < right.f) {
            return -1;
        }
 
        return 0;
    }
 
    // Manhattan heuristic for estimating the cost from a node to the goal
    function manhattan(node, goal) {
    return Math.abs(node.x - goal.x) + Math.abs(node.y - goal.y);
}
 
// Gets all the valid neighbour nodes of a given node (diagonals are not
// neighbours)
function getNeighbours(node) {
    var neighbours = [];
 
    for (var i = -1; i < 2; i++) {
        for (var j = -1; j < 2; j++) {
            // Ignore diagonals
            if (Math.abs(i) === Math.abs(j)) {
                continue;
            }
 
            // Ignore positions outside the grid
            if (node.x + i < 0 || node.y + j < 0 ||
                node.x + i >= grid[0].length || node.y + j >= grid.length) {
                continue;
            }
 
            if (grid[node.y + j][node.x + i] === 0) {
                neighbours.push(new GraphNode(node.x + i, node.y + j));
            }
        }
    }
 
    return neighbours;
}
 
// Builds the path needed to reach a target node from the pathfinding information
function calculatePath(parents, target) {
    var path = [];
 
    var node = target;
    while (typeof node !== "undefined") {
        path.push([node.x, node.y]);
        node = parents[node.hash];
    }
 
    return path.reverse();
}
 
// Searches for a path between two positions in a grid
this.search = function(start, end) {
    var fCosts = new BinaryHeap([], nodeComparer);
    var gCosts = {};
    var colors = {};
    var parents = {};
 
    // Initialization
    var node = new GraphNode(start[0], start[1]);
    var endNode = new GraphNode(end[0], end[1]);
    node.f = manhattan(node, endNode);
 
    fCosts.push(node);
    gCosts[node.hash] = 0;
 
    while (!fCosts.isEmpty) {
        var current = fCosts.pop();
 
        // Have we reached our goal?
        if (current.x === endNode.x && current.y === endNode.y)
        {
            return calculatePath(parents, endNode);
        }
 
        // Mark the node as visited (Black), and get check it's neighbours
        colors[current.hash] = "Black";
 
        var neighbours = getNeighbours(current);
        for (var i = 0; i < neighbours.length; i++)
        {
            var neighbour = neighbours[i];
 
            if (colors[neighbour.hash] === "Black") {
                continue;
        }
 
        // If we had not visited the neighbour before, or we have found a faster way to visit the neighbour, then update g and calculate f
        if (typeof gCosts[neighbour.hash] !== "Undefined" || gCosts[current.hash] + 1 < gCosts[neighbour.hash]) {
            parents[neighbour.hash] = current;
            gCosts[neighbour.hash] = gCosts[current.hash] + 1;
            neighbour.f = gCosts[neighbour.hash] + manhattan(neighbour, endNode);
 
            // Neighbour not visited before, mark it as a potential candidate ("Gray")
            if (typeof colors[neighbour.hash] !== "Undefined") {
                colors[neighbour.hash] = "Gray";
                fCosts.push(neighbour);
            }
            else { // We have found a better way to reach this node
                fCosts.decreaseItem(neighbour);
            }
        }
    }
}
 
return [];
};
}

New to Internet Explorer 10

Internet Explorer 10 is full of new api's that allow for a more native-app like expereince. Below is a list of ones we used in this experience and how they benefitted us.

  • Pointer Events – All interactions in the experience use the Pointer object to detect user input, handling mouse, pen, and touch events.
  • Gesture API - Using the Pointer API as it's basis, the Gesture API is used to handle more advanced pointer interactions in the site such as twisting the Safe dial with two fingers, and drawing the path in the Heist.
  • CSS3 Animations - CSS3 Animations were used in the site for some of the larger transition animations, both between pages, and between sections in the behind-the-scenes section. This allows the browser to handle the animation computations instead of programming it in JavaScript.
  • requestAnimationFrame - requestAnimationFrame allows us to get a frame for animation at whatever time the browser deems most appropriate instead of a set frame rate, which may drop frames all together if the browser becomes bogged down. This is used throughout the experience to deliver the smoothest animation possible.
  • pageVisibility API - Using the pageVisibility API, we are able to detect when the site is no longer the currently visible browser tab. This allows us to manage parts of the app that don't need to run when the user isn't actively using the site. We mainly used this feature to mute the site's audio when not visible.
  • SVG Filters - SVG Filters were used in the experience to create effects on some of the dynamically created portions of the site. These were mainly used to add drop-shadows that matched the site's overall style to items created in code, such as the wires that are drawn in the bomb game.
  • setImmediate API - The setImmediate API is used in various places around the site to improve the site's performance and power consumption. It's used as a timer, just as setInterval or setTimeout are used, with the added of benefit of being called as soon as the CPU is ready to process it. This balances speed and power consumption. It was used in places such as saving profile data, or other items that need to happen as fast as possible to keep the user experience smooth.

This article is part of the HTML5 tech series from the Internet Explorer team. Try-out the concepts in this article with 3 months of free BrowserStack cross-browser testing @ http://modern.IE.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionDidnt we learn anything? Pin
pip01022-Apr-13 22:18
pip01022-Apr-13 22:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.