Browser…​unplugged
Showcases of modern Browser technologies

September 30, 2013

Drag & drop with SVG

For the patience project, I had to fight some performance problems. Displaying a playing card as SVG grafic is in itself a completely harmless task for a browser. But as soon as you try to drag the card with the mouse across others, or when cards are displayed one on top of the other, as in a pile, delays get obvious; and the game flow is impeded.

🔗 Cards as individual SVG files

The D3 framework uses functions of data objects to create and manipulate DOM elements. This is not the place to describe the process in detail, a short introduction can be viewed here . In a first attempt, I bound objects containing the src url to <img> elements:

{
    key: // unique key for pile, card index and rendered card
    ref: "cards/card_(...).svg" // URL of the SVG file for the card
}

For every pile of cards these objects a collected in an array card_names.

First, the constructor for the class creating the gamepad defines a dynamic stylesheet to be able to adjust the rendered card sizes:

function Area(...) {
    ...
    var sheet = d3.select("head").append("style").property("sheet");
    sheet.insertRule("img {}", 0); // insert a dynamic style sheet for cards
    this.rule = sheet.cssRules[0];
    ...
}

Depending on the browser window real estate a scaling factor is determined:


Area.prototype.resize = function () {
    ...
    this.scale = ... // compute scaling factor
    this.card_w = Math.round(101 * this.scale); // actual card sizes
    this.card_h = Math.round(156 * this.scale);
    this.rule.style.width = this.card_w + "px"; // write sizes into the stylesheet
    this.rule.style.height = this.card_h + "px";
    ...
};

When assigning cards to their piles, the <img> elements are added to the pile and for newly appended cards the url link is retrieved and set from the data object:

Area.prototype.alter_pile = function (pile) {
    ...
    var stack = this.get_stack(pile); // DOM element of pile
    var card_names = to_view(pile); // assign d(ata) objekt for every card
    var gd = stack.inner.selectAll("img").data(card_names, function (d) {
        return d.key; // identifies pre-existing cards by their unique id
    });
    gd.enter().append("img");
    gd.attr("src", function(d) {
        return d.ref;
    });
    ...
};

The result is disappointing for all tested browsers - current versions of Chrome, Firefox and Opera, IE 9 and IE10. Cards being dragged with the mouse will move with a noticable delay. It is as if they "hop" out of their original position only after some time lag. Dropped onto another pile it takes several tenths of a second before they are rendered and the game gets ready for user interaction again.

🔗 Fallback: high resolution PNG files

The published variant exchanges the SVG files for PNG files with a relatively high resolution (200 × 308 pixel). That results in the considerable size of 1.3 MB for the totality of the cards, but the resulting load times can be alleviated for the most part by leveraging the application cache.

This is the sole solution that consistently enables fluent playing on all browsers. Nonetheless, it is contrived.

So, how do you get the scalability of SVGs to the client without wasting computing time for permanently repainting objects that in essence remain static? I have tested two scenarios:

🔗 Copy sprites to canvas elements

For this attempt the cards are gathered in a single SVG file. At initialization, it is loaded in an HTMLImageElement outside of the DOM tree. From there clippings containing just one card are rendered onto canvas elements.

The data object contains no longer an URL but the coordinates describing where in the sprite an individual card can be found:

{
    key: // unique key for pile, card index and rendered card
    ref_x: card.value - 1, // column index of the card in Karten.svg
    ref_y: base.suit_names.indexOf(card.suit), // row index
}

Since the sizing must be given as element attributes, the card size cannot be set via a dynamic stylesheet:

function Area(...) {
    ...
    this.karten = new Image(); // offscreen image
    d3.select(this.karten).on("load", function() {
        ...
    });
    this.karten.src = "cards/Karten.svg"; // file contains all cards in a grid
}

Area.prototype.resize = function () {
    ...
    this.scale = ...                            // compute scaling factor
    this.card_w = Math.round(101 * this.scale); // actual card sizes
    this.card_h = Math.round(156 * this.scale);
    ... // draw all existing cards again
};

Area.prototype.alter_pile = function (pile) {
    ...
    var stack = this.get_stack(pile); // DOM element of pile
    var card_names = to_view(pile);   // assign d(ata) objekt for every card
    var gd = stack.inner.selectAll("canvas").data(card_names, function (d) {
        return d.key; // identifies pre-existing cards by their unique id
    });
    var self = this;
    gd.enter().append("canvas")
        .attr("width", this.card_w).attr("height", this.card_h) // assign actual card size
        .each(function (d) {
            var ctx = this.getContext('2d'); // this references the current canvas element
            ctx.clearRect(0, 0, self.card_w, self.card_h);  // clear and draw again
            ctx.drawImage(self.karten, d.ref_x * 101, d.ref_y * 156, 101, 156, 0, 0,
                self.card_w, self.card_h);
        });
    ...
};
Screenshot
In Chrome SVG patterns blur when rendered with canvas

This solution works (almost) flawlessly with Chrome. Gameplay is fluent and the card are rendered in reasonable times on scaling. Unfortunately one error muddies the water: <svg:pattern> fills refuse to scale and are rendered blurred on magnification.

Internet Explorer renders the cards error-free, rendering times are suboptimal, but can be tolerated.

Screenshot
Unscaled transfer from SVG leads to blurred rendering on canvas in Firefox

Firefox fails to scale the clippings and shows the cards always in their base resolution of 101 × 156 pixel.

Opera fails deplorably. neither images are rendered offscreen, nor it seems canvas.drawImage() is implemented.

🔗 SVG as background image

This attempt relies on CSS. Cards are displayed as background images in <div> elements. Consequently the data object has URL in functional notation:

{
    key: // unique key for pile, card index and rendered card
    ref: "url(cards/card_(...).svg)" // funcURL of the SVG file for the card
}

Es kommt wieder ein dynamisches Stylesheet zum Einsatz, beim Aktualisieren der Stapel muss nur das Hintergrundbild ausgetauscht werden:

function Area(...) {
    ...
    var sheet = d3.select("head").append("style").property("sheet");
    sheet.insertRule("div.bgcard {}", 0); // insert a dynamic style sheet for cards
    this.rule = sheet.cssRules[0];
    ...
}

Area.prototype.resize = function() {
    ...
    this.scale = ...                            // compute scaling factor
    this.card_w = Math.round(101 * this.scale); // actual card sizes
    this.card_h = Math.round(156 * this.scale);
    this.rule.style.width = this.card_w + "px"; // write sizes into the stylesheet
    this.rule.style.height = this.card_h + "px";
    this.rule.style.backgroundSize = this.card_w + "px";
    ...
};

Area.prototype.alter_pile = function(pile) {
    ...
    var stack = this.get_stack(pile); // DOM element of pile
    var card_names = to_view(pile);   // assign d(ata) objekt for every card
    var gd = stack.inner.selectAll("div").data(card_names, function (d) {
        return d.key; // identifies pre-existing cards by their unique id
    });
    var self = this;
    gd.enter().append("div").classed("bgcard", true)
        .style("background-image", function (d) {
            return d.ref;
        });
};
Screenshot
Opera sometimes fails to renew the screen buffer, so that some cards seem to miss

Again Chrome ranks best and shows everything fluently and correct.

Internet Explorer requires Version 10, in Version 9 some CSS3 features are missing.

Firefox has a devastating performance. On loading a game the maximum execution time for scripts was surpassed on multiple occasions.

Opera produces multiple errors: Images aren't re-rendered on scaling, and on rendering card images for the first time it is possible they won't show on the screen at all until the whole screen buffer is written again.