2017-01-18

實作 HTML5 Gamepad API

最近再次拿起手掣玩遊戲,希望可以在錄製影像同時紀錄玩遊戲按按鈕等操作
讓觀眾可以欣賞玩家進行遊戲,同時可以了解玩家如何操作
最初使用 Java 希望可以達到跨平台效果,但發現有點麻煩
最後發現 Firefox 支援一個稱為 Gamepad API 的實驗性 API 可以達到相似效果


實際上 Gamepad API 已經開發了一段時間,早在 Firefox 24 已經投入其中
Firefox 可以到 about:config 的 dom.gamepad.enabled 啟動
Chrome 可以到 chrome://flags 的 enable-gamepad-extensions 啟動
由於必須支援 HTML5 ,因此編程上大可不用理會相容性,直接使用只支援 HTML5 的 HTML元件 及 Javascript語法 即可

由於手掣的動作在 Javascript 都是一種 event 因此先以
window.addEventListener('gamepadconnected', function(event){
    console.log(event.gamepad);
});
便可以偵測到已經連接的手掣
但不同瀏覽器對 gamepadconnect event 的實作方法都不一樣
  • 有些會在連接著打開網頁便可以立即偵測到
  • 有些需要不連接著,要開啟網頁後,連接時才能偵測到
  • 有些會在連接後,按下手掣上某個觸法按鈕,才能偵測到

能夠偵測連接手掣,亦有能夠偵測取消連接的 event
window.addEventListener('gamepaddisconnected', function(event){
    console.log(event.gamepad);
});
gamepaddisconnect event 則比較統一獲取已連接的手掣資訊

每一個手掣資訊最基本會包括:

id
手掣的類別名稱 作業系統、瀏覽器、手掣驅動程式會影響類別名稱
index
手掣在系統中的編號
connected
手掣是否連接系統
buttons
手掣的按鈕資訊
axes
手掣的轉軸資訊

當中的 gamepad.buttons 及 gamepad.axes 都是傳回 array
gamepad.buttons 會列出手掣上所有按鈕,由 0 開始
gamepad.axes 會列出手掣上所有轉軸,由 0 開始
預設的 buttons 中的 GamepadButton 物件有 pressed 及 value 的屬性,根據不同性質使用不同屬性
當 button
pressed 為 true 與 value 為 1 是相同,都是按下的意思
pressed 為 false 與 value 為 0 是相同,都是沒有按下的意思
可是不是所有瀏覽器對 button 都是 GamepadButton 的類別
Firefox 是以 GamepadButton 類別,可是 Chrome 則是以 DOMNumber 類別
在 Chrome 中,button 只會以數值傳回
若 button 為 0 是沒有按下,若 button 為大於 0 是按下

對於 axis (這裡的 axes 的單數不是 axe)
由於 axis 不能單純地表達按下及沒有按下的狀態 (不是指方向掣 (D-Pad) 的上下左右方向) 而是轉軸的位置
使用 方向掣 與 類比(Analog) 操作,因此本質相同,但概念上不同
axis 是介乎 -1 至 1 之間的資料, x 軸數值越小越偏向左,越大越偏向右,y 軸數值越小越偏向上,越大越偏向下
使用方向掣,例如按下左按鈕,實際是將 x 軸數設定為 -1 (或少於 0 的數值)

部分瀏覽器對 D-Pad 的操作會當成 button 的概念進行檢查
由於現在有不少手掣的 Trigger (PS3 的 L2 及 R2 按鈕或 XBox360 的 LT 及 RT 按鈕) 都有壓力感應模擬
會當作 axis 概念進行檢查,不使用時是 -1 而不是 0

由於需要不斷檢查 event 的狀態,可是使用如 for, while 等迴圈 (looping) 會因為太快不能顯示互動效果 (實際應該會 loop 死)
因此會使用 window.setTimeout 或 window.setInterval 並設定間距時間來模擬迴圈
但由於使用 window.setTimeout 或 window.setInterval 會因為固定了間距時間的數值
若果間距時間太少會佔用資源,但數值太大又不能產生即時互動的效果
幸好 Firefox 及其他瀏覽器陸續都支援 W3C 提出 HTML5 的 window.requestAnimationFrame
一般情況下 window.requestAnimationFrame 會以 60FPS (1秒60幀 ~ 0.016秒) 的速度更新,而且還會根據資源使用量調整 FPS 值
而使用 Gamepad API 必須使用 HTML5 ,因此建議使用 window.requestAnimationFrame 效果會比較好

但在下發現不同瀏覽器對 Gamepad 都有不同
Firefox 的 Gamepad 物件當 手掣 有變化時直接更新 Gamepad 中的屬性資料
但 Chrome 的 Gamepad 物件則不會更新內容,需要使用 navigator.getGamepads(); 不斷接收 Gamepad 物件的新資料
然而,如果同一電腦超過一個網頁,便只有第一個存取手掣的資料網頁能夠持續存取手掣的資料
另外, navigator.getGamepads(); 在不同瀏覽器又有不同結果, Firefox 會傳回 array ,Chrome 會傳回 GamepadList
不過兩者都可以使用 JavaScript 的 for each 語法列出內容,因此影響不大

較舊版的 Firefox 及 Chrome 因為實作方式更不統一
window.requestAnimationFrame 及 navigator.getGamepads 可能需要使用前綴才能使用這些功能
Firefox 需要使用 window.mozRequestAnimationFrame 及 navigator.mozGetGamepads
Chrome 則需要使用 window.webkitRequestAnimationFrame 及 navigator.webkitGetGamepads
若想避免意外,可以使用
if (!!window.requestAnimationFrame){
    window.requestAnimationFrame(function(){
        // do something
    });
else if (!!window.mozRequestAnimationFrame){
    window.mozRequestAnimationFrame(function(){
        // do something
    });
else if (!!window.webkitRequestAnimationFrame){
    window.webkitRequestAnimationFrame(function(){
        // do something
    });
}

var gamepads = null;
if (!!navigator.getGamepads){
    gamepads = navigator.getGamepads();
else if (!!navigator.mozGetGamepads){
    gamepads = navigator.mozGetGamepads();
else if (!!navigator.webkitGetGamepads){
    gamepads = navigator.webkitGetGamepads();
}
if (gamepads != null){
    // do something
}

Javascript
(在下將不同功用的 JavaScript 分開,方便改動)

先前提及過不同作業系統、瀏覽器、手掣驅動程式會影響效果
因此需要根據不同作業系統、瀏覽器、手掣驅動程式而調整內容

default-driver.js
var drivers = {};

android-chrome-xbox360-0.js
if (!("Android" in drivers)){
    drivers["Android"] = {};
}
if (!("Chrome" in drivers["Android"])){
    drivers["Android"]["Chrome"] = {};
}
drivers["Android"]["Chrome"]["Microsoft X-Box 360 pad"] = {
    "style": "xbox360",
    "buttons": {
        "0": "cr",
        "1": "ci",
        "2": "sq",
        "3": "tr",
        "4": "l1",
        "5": "r1",
        "6": "l2",
        "7": "r2",
        "8": "se",
        "9": "st",
        "10": "l3",
        "11": "r3",
        "12": "du",
        "13": "dd",
        "14": "dl",
        "15": "dr",
        "16": "ho"
    },
    "axes": {
        "0": {"type": "analog", "key": "l3", "dimension": "x"},
        "1": {"type": "analog", "key": "l3", "dimension": "y"},
        "2": {"type": "analog", "key": "r3", "dimension": "x"},
        "3": {"type": "analog", "key": "r3", "dimension": "y"}
    }
};

android-firefox-xbox360-0.js
if (!("Android" in drivers)){
    drivers["Android"] = {};
}
if (!("Firefox" in drivers["Android"])){
    drivers["Android"]["Firefox"] = {};
}
drivers["Android"]["Firefox"]["android"] = {
    "style": "xbox360",
    "buttons": {
        "0": "cr",
        "1": "ci",
        "2": "sq",
        "3": "tr",
        "4": "l1",
        "5": "r1",
        "6": "l2",
        "7": "r2",
        "8": "se",
        "9": "st",
        "10": "l3",
        "11": "r3",
        "12": "du",
        "13": "dd",
        "14": "dl",
        "15": "dr",
        "16": "ho"
    },
    "axes": {
        "0": {"type": "analog", "key": "l3", "dimension": "x"},
        "1": {"type": "analog", "key": "l3", "dimension": "y"},
        "2": {"type": "analog", "key": "r3", "dimension": "x"},
        "3": {"type": "analog", "key": "r3", "dimension": "y"}
    }
};

linux-chrome-xbox360-0.js
if (!("Linux" in drivers)){
    drivers["Linux"] = {};
}
if (!("Chrome" in drivers["Linux"])){
    drivers["Linux"]["Chrome"] = {};
}
drivers["Linux"]["Chrome"]["Microsoft Inc. Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"] = {
    "style": "xbox360",
    "buttons": {
        "0": "cr",
        "1": "ci",
        "2": "sq",
        "3": "tr",
        "4": "l1",
        "5": "r1",
        "6": "l2",
        "7": "r2",
        "8": "se",
        "9": "st",
        "10": "l3",
        "11": "r3",
        "12": "du",
        "13": "dd",
        "14": "dl",
        "15": "dr",
        "16": "ho"
    },
    "axes": {
        "0": {"type": "analog", "key": "l3", "dimension": "x"},
        "1": {"type": "analog", "key": "l3", "dimension": "y"},
        "2": {"type": "analog", "key": "r3", "dimension": "x"},
        "3": {"type": "analog", "key": "r3", "dimension": "y"}
    }
};

linux-chrome-xbox360-1.js
(在 Linux 使用 Chrome 連接 XBox360 手掣有兩種驅動程式)
if (!("Linux" in drivers)){
    drivers["Linux"] = {};
}
if (!("Chrome" in drivers["Linux"])){
    drivers["Linux"]["Chrome"] = {};
}
drivers["Linux"]["Chrome"]["Inno GamePad.. Inno GamePad.. (STANDARD GAMEPAD Vendor: 045e Product: 028e)"] = {
    "style": "xbox360",
    "buttons": {
        "0": "cr",
        "1": "ci",
        "2": "sq",
        "3": "tr",
        "4": "l1",
        "5": "r1",
        "6": "l2",
        "7": "r2",
        "8": "se",
        "9": "st",
        "10": "l3",
        "11": "r3",
        "12": "du",
        "13": "dd",
        "14": "dl",
        "15": "dr",
        "16": "ho"
    },
    "axes": {
        "0": {"type": "analog", "key": "l3", "dimension": "x"},
        "1": {"type": "analog", "key": "l3", "dimension": "y"},
        "2": {"type": "analog", "key": "r3", "dimension": "x"},
        "3": {"type": "analog", "key": "r3", "dimension": "y"}
    }
};

linux-firefox-xbox360-0.js
if (!("Linux" in drivers)){
    drivers["Linux"] = {};
}
if (!("Firefox" in drivers["Linux"])){
    drivers["Linux"]["Firefox"] = {};
}
drivers["Linux"]["Firefox"]["045e-028e-Microsoft X-Box 360 pad"] = {
    "style": "xbox360",
    "buttons": {
        "0": "cr",
        "1": "ci",
        "2": "sq",
        "3": "tr",
        "4": "l1",
        "5": "r1",
        "6": "se",
        "7": "st",
        "8": "ho",
        "9": "l3",
        "10": "r3"
    },
    "axes": {
        "0": {"type": "analog", "key": "l3", "dimension": "x"},
        "1": {"type": "analog", "key": "l3", "dimension": "y"},
        "2": {"type": "zbutton", "key": "l2", "dimension": "z"},
        "3": {"type": "analog", "key": "r3", "dimension": "x"},
        "4": {"type": "analog", "key": "r3", "dimension": "y"},
        "5": {"type": "zbutton", "key": "r2", "dimension": "z"},
        "6": {"type": "dpad", "key": {"<": "dl", ">": "dr"}, "dimension": "x"},
        "7": {"type": "dpad", "key": {"<": "du", ">": "dd"}, "dimension": "y"}
    }
};

windows-chrome-xbox360-0.js
if (!("Windows" in drivers)){
    drivers["Windows"] = {};
}
if (!("Chrome" in drivers["Windows"])){
    drivers["Windows"]["Chrome"] = {};
}
drivers["Windows"]["Chrome"]["Xbox 360 Controller (XInput STANDARD GAMEPAD)"] = {
    "style": "xbox360",
    "buttons": {
        "0": "cr",
        "1": "ci",
        "2": "sq",
        "3": "tr",
        "4": "l1",
        "5": "r1",
        "6": "l2",
        "7": "r2",
        "8": "se",
        "9": "st",
        "10": "l3",
        "11": "r3",
        "12": "du",
        "13": "dd",
        "14": "dl",
        "15": "dr",
        "16": "ho"
    },
    "axes": {
        "0": {"type": "analog", "key": "l3", "dimension": "x"},
        "1": {"type": "analog", "key": "l3", "dimension": "y"},
        "2": {"type": "analog", "key": "r3", "dimension": "x"},
        "3": {"type": "analog", "key": "r3", "dimension": "y"}
    }
};

windows-firefox-xbox360-0.js
if (!("Windows" in drivers)){
    drivers["Windows"] = {};
}
if (!("Firefox" in drivers["Windows"])){
    drivers["Windows"]["Firefox"] = {};
}
drivers["Windows"]["Firefox"]["xinput"] = {
    "style": "xbox360",
    "buttons": {
        "0": "cr",
        "1": "ci",
        "2": "sq",
        "3": "tr",
        "4": "l1",
        "5": "r1",
        "6": "l2",
        "7": "r2",
        "8": "se",
        "9": "st",
        "10": "l3",
        "11": "r3",
        "12": "du",
        "13": "dd",
        "14": "dl",
        "15": "dr"
    },
    "axes": {
        "0": {"type": "analog", "key": "l3", "dimension": "x"},
        "1": {"type": "analog", "key": "l3", "dimension": "y"},
        "2": {"type": "analog", "key": "r3", "dimension": "x"},
        "3": {"type": "analog", "key": "r3", "dimension": "y"}
    }
};

generic-gamepad.js
function GenericGamepad(gamepad, driver){
    var _driver = "" + gamepad.id;
    var _index = "" + gamepad.index;
    var _id = _driver + "[" + _index + "]";
    var _style = null;
    var _buttons = {};
    var _analogs = {};
    var _dpads = {};
    var _zbuttons = {};
    var _values = {};
    if (driver != null){
        _style = driver.style;
        for (var i in driver.buttons){
            _buttons[i] = new GenericGamepadButton(i, driver.buttons[i], gamepad.buttons[i].value);
        };
        for (var i in driver.axes){
            var axis = driver.axes[i];
            if (axis.type == "analog"){
                _analogs[i] = new GenericGamepadAnalog(i, axis.key, axis.dimension, gamepad.axes[i]);
            } else if (axis.type == "dpad"){
                _dpads[i] = new GenericGamepadDPad(i, axis.key, gamepad.axes[i]);
            } else if (axis.type == "zbutton"){
                _zbuttons[i] = new GenericGamepadZButton(i, axis.key, gamepad.axes[i]);
            } else if (axis.type == "value"){
                _values[i] = new GenericGamepadValue(i, axis.key, gamepad.axes[i]);
            }
        };
    }
    this.getDriver = function(){ return _driver; };
    this.getIndex = function(){ return _index;} ;
    this.getId = function(){ return _id; };
    this.getStyle = function(){ return _style; };
    this.getButtons = function(){ return _buttons; };
    this.getAnalogs = function(){ return _analogs; };
    this.getDPads = function(){ return _dpads; };
    this.getZButtons = function(){ return _zbuttons; };
    this.getValues = function(){ return _values; };
}

generic-gamepad-button.js
function GenericGamepadButton(index, reference, value){
    var _index = index;
    var _reference = reference;
    var _value = value;
    this.getIndex = function(){ return _index; };
    this.getReference = function(){ return _reference; };
    this.getValue = function(){ return _value; };
    this.isPressed = function(){ return this.getValue() > 0; };
}

generic-gamepad-analog.js
function GenericGamepadAnalog(index, reference, dimension, value){
    var _index = index;
    var _reference = reference;
    var _dimension = dimension;
    var _value = value;
    this.getIndex = function(){ return _index; };
    this.getReference = function(){ return _reference; };
    this.getDimension = function(){ return _dimension; };
    this.getValue = function(){ return _value; };
}

generic-gamepad-dpad.js
function GenericGamepadDPad(index, references, value){
    var _index = index;
    var _references = references;
    var _value = value;
    this.getIndex = function(){ return _index; };
    this.getReferences = function(){ return _references; };
    this.getValue = function(){ return _value; };
    this.getDirection = function(){
        if (this.getValue() < 0){
            return "<";
        } else if (this.getValue() > 0){
            return ">";
        } else {
            return "=";
        }
    };
}

generic-gamepad-zbutton.js
function GenericGamepadZButton(index, reference, value){
    var _index = index;
    var _reference = reference;
    var _value = value;
    this.getIndex = function(){ return _index; };
    this.getReference = function(){ return _reference; };
    this.getValue = function(){ return _value; };
    this.isPressed = function(){ return this.getValue() > -1; };
}

generic-gamepad-value.js
function GenericGamepadValue(index, references, value){
    var _index = index;
    var _references = references;
    var _value = value.toFixed(7);
    _value = _value.substring(0, _value.length - 1);
    this.getIndex = function(){ return _index; };
    this.getReferences = function(){ return _references; };
    this.getValue = function(){ return _value; };
}

user-agent.js
網上可能有更準確的 JavaScript User Agent Detector 來檢查作業系統或瀏覽器
function UserAgent(){
    var userAgent = navigator.userAgent.toLowerCase();
    var _os = null;
    if (userAgent.indexOf("win") >= 0){
        _os = "Windows";
    } else if (userAgent.indexOf("android") >= 0){
        _os = "Android";
    } else if (userAgent.indexOf("linux") >= 0){
        _os = "Linux";
    }
    var _browser = null;
    if (userAgent.indexOf("firefox") >= 0){
        _browser = "Firefox";
    } else if (userAgent.indexOf("chrome") >= 0){
        _browser = "Chrome";
    }
    this.getOS = function(){ return _os; };
    this.getBrowser = function(){ return _browser; };
}

gamepad-detector.js
function GamepadDetector(gamepadContainer, drivers){
    var userAgent = new UserAgent();
    console.log("Your OS is " + userAgent.getOS());
    console.log("Your browser is " + userAgent.getBrowser());
    var _gamepads = {};
    var _getDrivers = function(){
        var os = userAgent.getOS();
        var browser = userAgent.getBrowser();
        if (os in drivers && browser in drivers[os]){
            return drivers[os][browser];
        } else {
            return null;
        }
    };
    var _setGamepad = function(gamepad){
        var availableDrivers = _getDrivers();
        if (gamepad instanceof Gamepad && availableDrivers != null && gamepad.id in availableDrivers){
            var gamepad = new GenericGamepad(gamepad, availableDrivers[gamepad.id]);
            var id = gamepad.getId();
            var gamepadTemplate = document.getElementById(id);
            _gamepads[id] = gamepad;
            if (gamepadTemplate == null){
                gamepadTemplate = document.getElementById("xbox360-gamepad-template").cloneNode(true);
                gamepadTemplate.setAttribute("id", id);
                gamepadContainer.appendChild(gamepadTemplate);
            }
        }
    };
    var _removeGamepad = function(gamepad){
        if (gamepad instanceof Gamepad){
            var id = gamepad.id + "[" + gamepad.index + "]";
            var template = document.getElementById(id);
            if (template != null){
                template.parentNode.removeChild(template);
                delete _gamepads[id];
            }
        }
    };
    var _updateGamepads = function(){
        for (var i in _gamepads){
            var gamepad = _gamepads[i];
            var template = document.getElementById(gamepad.getId());
            var buttons = gamepad.getButtons();
            var analogs = gamepad.getAnalogs();
            var dpads = gamepad.getDPads();
            var zbuttons = gamepad.getZButtons();
            var values = gamepad.getValues();
            _updateGamepadButtons(template, buttons);
            _updateGamepadAnalogs(template, analogs);
            _updateGamepadDPads(template, dpads);
            _updateGamepadZButtons(template, zbuttons);
            _updateGamepadValues(template, values);
        }
        var gamepads = navigator.getGamepads();
        for (var i in gamepads){
            _setGamepad(gamepads[i]);
        }
        window.requestAnimationFrame(function(){
            _updateGamepads();
        });
    };
    var _referenceToElement = function(template, reference){
        return template.getElementsByClassName("template " + reference)[0];
    }
    var _updateGamepadButtons = function(template, buttons){
        for (var i in buttons){
            var button = buttons[i];
            _updateGamepadButton(template, button);
        }
    };
    var _updateGamepadButton = function(template, button){
        var element = _referenceToElement(template, button.getReference());
        if (element != null){
            element.setAttribute("fill-opacity", ((button.isPressed()) ? 1 : 0));
        }
    };
    var _updateGamepadAnalogs = function(template, analogs){
        for (var i in analogs){
            var analog = analogs[i];
            _updateGamepadAnalog(template, analog);
        }
    };
    var _updateGamepadAnalog = function(template, analog){
        var element = _referenceToElement(template, analog.getReference());
        if (element != null){
            var shadowElement = _referenceToElement(template, analog.getReference() + "-shadow");
            var baseElement = _referenceToElement(template, analog.getReference() + "-base");
            var name = "c" + analog.getDimension();
            var baseValue = baseElement.getAttribute(name);
            var value = baseValue - (-10 * analog.getValue());
            element.setAttribute(name, value);
            shadowElement.setAttribute(name, value);
        }
    };
    var _updateGamepadDPads = function(template, dpads){
        for (var i in dpads){
            var dpad = dpads[i];
            _updateGamepadDPad(template, dpad);
        }
    };
    var _updateGamepadDPad = function(template, dpad){
        var references = dpad.getReferences();
        var direction = dpad.getDirection();
        for (var i in references){
            var element = _referenceToElement(template, references[i]);
            if (element != null){
                element.setAttribute("fill-opacity", 0);
            }
        }
        if (direction != "="){
            var element = _referenceToElement(template, references[direction]);
            if (element != null){
                element.setAttribute("fill-opacity", 1);
            }
        }
    };
    var _updateGamepadZButtons = function(template, zbuttons){
        for (var i in zbuttons){
            var zbutton = zbuttons[i];
            _updateGamepadZButton(template, zbutton);
        }
    };
    var _updateGamepadZButton = function(template, zbutton){
        var element = _referenceToElement(template, zbutton.getReference());
        if (element != null){
            element.setAttribute("fill-opacity", ((zbutton.isPressed()) ? 1 : 0));
        }
    };
    var _updateGamepadValues = function(template, values){
        for (var i in values){
            var value = values[i];
            _updateGamepadValue(template, value);
        }
    };
    var _updateGamepadValue = function(template, value){
        _resetGamepadDPads(template);
        var references = value.getReferences();
        for (var i in references){
            var reference = references[i];
            if (i == value.getValue()){
                for (var j in reference){
                    var element = template.getElementsByClassName("template " + reference[j])[0];
                    if (element != null){
                        element.setAttribute("fill-opacity", 1);
                    }
                }
            }
        }
    };
    var _resetGamepadDPads = function(template){
        var defaultDPadReferences = ["du", "dd", "dl", "dr"];
        for (var i in defaultDPadReferences){
            var defaultDPadReference = defaultDPadReferences[i];
            var element = template.getElementsByClassName("template " + defaultDPadReference)[0];
            if (element != null){
                element.setAttribute("fill-opacity", 0);
            }
        }
    };
    this.start = function(){
        _updateGamepads();
    };
    window.addEventListener("gamepadconnected", function(event){
        var gamepad = event.gamepad;
        var availableDrivers = _getDrivers();
        if (gamepad instanceof Gamepad && availableDrivers != null && gamepad.id in availableDrivers){
            _setGamepad(gamepad);
            console.log("Gamepad: \"" + gamepad.id + "[" + gamepad.index + "]" + "\" is connected.");
        }
    });
    window.addEventListener("gamepaddisconnected", function(event){
        var gamepad = event.gamepad;
        if (gamepad instanceof Gamepad){
            _removeGamepad(gamepad);
            console.log("Gamepad: \"" + gamepad.id + "[" + gamepad.index + "]" + "\" is disconnected.");
        }
    });
}

initial.js
window.addEventListener("load", function(event){
    var gamepadContainer = document.getElementById("gamepad-container");
    var gamepadDetector = new GamepadDetector(gamepadContainer, drivers);
    gamepadDetector.start();
});

xbox360.svg
(網上有很多 XBox360 Gamepad SVG ,但在下將對視覺上不太影響的元件刪除,並加入需要的功能)
<svg width="320" height="203.21" version="1.1" xmlns="http://www.w3.org/2000/svg">
    <g stroke="#101010">
        <g fill="#c0c0c0">
            <path d="m303.27 102.26s-12.013-35.979-18.784-45.907c-8.9447-13.117-22.467-27.89-27.536-29.977-5.0685-2.0867-21.467-2.4459-25.641-0.65743-4.1739 1.7887-17.793 13.976-28.228 16.956-10.436 2.9808-71.548 1.4905-79.896 0.29821-8.3483-1.1923-12.558-1.4293-19.713-6.1988-7.1557-4.7695-10.724-10.626-13.11-11.819-2.3849-1.1923-19.854-1.0256-24.326 0.76262-4.4724 1.7887-19.31 15.957-30.341 34.141-11.032 18.184-22.799 58.132-23.923 67.441-2.0872 17.29-1.0871 31.389-0.56118 34.281 0.52594 2.8927 4.4597 22.785 19.178 28.977 19.07 8.0225 46.197-8.4519 65.61-16.057 20.23-7.9244 24.991-7.2567 39.112-7.2567 28.026 0 25.933-0.29794 53.966-0.29794 28.623 0 52.44 15.072 65.559 20.139s22.73 8.706 35.253 0.65744c12.522-8.0485 17.556-23.48 19.345-38.981s-5.9629-46.504-5.9629-46.504z"/>
            <ellipse cx="114.96" cy="145.87" rx="33.507" ry="27.215"/>
            <ellipse cx="203.74" cy="146.24" rx="30.847" ry="22.899"/>
            <path d="m71.356 78.168c-17.88-3.1638-34.006 5.3862-36.019 19.098-2.0125 13.711 10.85 27.391 28.73 30.555 17.88 3.1638 34.006-5.3862 36.019-19.098 2.0128-13.711-10.85-27.391-28.73-30.555z"/>
            <ellipse cx="114.54" cy="149.66" rx="28.466" ry="23.566"/>
        </g>
        <g fill="#404040">
            <ellipse cx="203.28" cy="150.55" rx="21.336" ry="15.425"/>
            <ellipse cx="115.01" cy="150.17" rx="24.069" ry="19.822"/>
            <ellipse cx="68.214" cy="107.63" rx="21.336" ry="15.425"/>
        </g>
        <path d="m199.43 47.373c-1.5347 0.37973-72.974 1.0892-78.042-0.22379-13.028-3.3745-24.604-6.6677-24.604-6.6677v0.59379s7.9628 2.6781 24.38 6.9685c6.3442 1.658 72.615 1.3414 78.265 0.22353 15.358-3.0386 27.393-7.1544 27.393-7.1544v-0.8941c0-2.63e-4 -12.441 3.4547-27.393 7.1542z" fill="#101010" stroke-width=".5"/>
        <path d="m227.55 54.903s-18.029 9.9795-31.862 15.539c-1.3575 0.54567-17.687 1.1203-34.671 0.96353-17.012-0.15673-34.679-1.0464-37.025-2.1787-12.269-5.9227-28.463-15.059-28.463-15.059v0.83362s11.384 6.8494 27.596 14.972c2.9963 1.5013 20.379 2.0483 37.795 2.2445 16.441 0.18487 32.855-0.076 35.478-1.0514 15.831-5.8874 31.76-15.71 31.76-15.71z" fill="#101010" stroke-width=".5"/>
    </g>
    <g>
        <ellipse cx="161.42" cy="104.72" rx="18.546" ry="15.049" fill="#40f000"/>
        <rect x="160.34" y="89.567" width="1.8737" height="31.574" fill="#c0c0c0"/>
        <rect x="141.55" y="104.42" width="39.447" height="1.8737" fill="#c0c0c0"/>
    </g>
    <ellipse cx="114.46" cy="152.67" rx="22.284" ry="18.066" fill="#808080" stroke-width="0"/>
    <path d="m135.97 147.57s-7.5015-0.17541-10.542-3.4323c-3.0405-3.2572-2.9148-8.4643-2.9148-8.4643s-4.2086-1.0687-8.0464-1.1755c-3.978-0.11045-7.765 1.1794-7.765 1.1794s0.43416 5.5456-2.606 8.8027c-3.0405 3.2569-10.854 2.7849-10.854 2.7849s-1.0519 2.6179-1.0519 5.3326c0 2.9229 1.2273 6.0457 1.2273 6.0457s7.6806-0.51805 10.504 2.305c2.8233 2.8225 2.5185 8.6313 2.5185 8.6313s4.5573 1.227 8.733 1.227c4.2922 0 8.7322-1.753 8.7322-1.753s-0.68951-2.7191 2.5682-7.4958c3.2574-4.7769 9.6311-4.3509 9.6311-4.3509s0.57854-2.1548 0.63192-4.6112c0.0553-2.5358-0.76525-5.0257-0.76525-5.0257z" fill="#808080" stroke-width="0"/>
    <g>
        <ellipse cx="114.46" cy="152.67" rx="22.284" ry="18.066" fill-opacity="0" stroke="#101010"/>
        <path d="m135.97 147.57s-7.5015-0.17541-10.542-3.4323c-3.0405-3.2572-2.9148-8.4643-2.9148-8.4643s-4.2086-1.0687-8.0464-1.1755c-3.978-0.11045-7.765 1.1794-7.765 1.1794s0.43416 5.5456-2.606 8.8027c-3.0405 3.2569-10.854 2.7849-10.854 2.7849s-1.0519 2.6179-1.0519 5.3326c0 2.9229 1.2273 6.0457 1.2273 6.0457s7.6806-0.51805 10.504 2.305c2.8233 2.8225 2.5185 8.6313 2.5185 8.6313s4.5573 1.227 8.733 1.227c4.2922 0 8.7322-1.753 8.7322-1.753s-0.68951-2.7191 2.5682-7.4958c3.2574-4.7769 9.6311-4.3509 9.6311-4.3509s0.57854-2.1548 0.63192-4.6112c0.0553-2.5358-0.76525-5.0257-0.76525-5.0257z" fill-opacity="0" stroke="#101010"/>
        <g fill="#ff0000" fill-opacity=".01">
            <path class="template du" d="m125.42 144.14c-3.0405-3.2572-2.9148-8.4643-2.9148-8.4643s-4.2086-1.0687-8.0464-1.1755c-3.978-0.11045-7.765 1.1794-7.765 1.1794s0.43416 5.5456-2.606 8.8027z"/>
            <path class="template dd" d="m103.92 160.95c2.8233 2.8225 2.5185 8.6313 2.5185 8.6313s4.5573 1.227 8.733 1.227c4.2922 0 8.7322-1.753 8.7322-1.753s-0.68951-2.7191 2.5682-7.4958z"/>
            <path class="template dl" d="m104.09 144.48c-3.0405 3.2569-10.854 2.7849-10.854 2.7849s-1.0519 2.6179-1.0519 5.3326c0 2.9229 1.2273 6.0457 1.2273 6.0457s7.6806-0.51805 10.504 2.305z"/>
            <path class="template dr" d="m135.97 147.57s-7.5015-0.17541-10.542-3.4323l1.0443 17.42c3.2574-4.7769 9.6311-4.3509 9.6311-4.3509s0.57854-2.1548 0.63192-4.6112c0.0553-2.5358-0.76525-5.0257-0.76525-5.0257z"/>
        </g>
        <g fill="#101010">
            <path d="m113.1 89.186v2.6737h1.5837q0.79673 0 1.178-0.32749 0.38615-0.33238 0.38615-1.0118 0-0.68431-0.38615-1.0069-0.38126-0.32749-1.178-0.32749zm0-3.0012v2.1996h1.4615q0.72341 0 1.0754-0.26884 0.35681-0.27372 0.35681-0.83095 0-0.55234-0.35681-0.82606-0.35194-0.27372-1.0754-0.27372zm-0.98736-0.8114h2.5222q1.1291 0 1.7401 0.46924t0.61099 1.3344q0 0.66965-0.31283 1.0656-0.31283 0.39592-0.91893 0.49368 0.7283 0.15641 1.1291 0.65498 0.40571 0.49368 0.40571 1.2366 0 0.97759-0.66477 1.5104-0.66476 0.53278-1.8916 0.53278h-2.6199z"/>
            <path d="m121.42 89.919q-1.09 0-1.5104 0.24929-0.42036 0.24928-0.42036 0.8505 0 0.47902 0.31283 0.76252 0.31771 0.27861 0.86027 0.27861 0.74786 0 1.1976-0.5279 0.45457-0.53278 0.45457-1.4126v-0.20041zm1.7939-0.37148v3.1234h-0.89938v-0.83095q-0.30794 0.49857-0.7674 0.73808-0.45946 0.23462-1.1242 0.23462-0.84072 0-1.3393-0.46924-0.49368-0.47413-0.49368-1.266 0-0.92382 0.61588-1.3931 0.62077-0.46924 1.8476-0.46924h1.2611v-0.08798q0-0.62077-0.41058-0.95804-0.4057-0.34216-1.1438-0.34216-0.46924 0-0.91404 0.11242t-0.85539 0.33727v-0.83095q0.49368-0.19063 0.95803-0.2835 0.46436-0.09776 0.90427-0.09776 1.1878 0 1.7743 0.61588 0.58655 0.61588 0.58655 1.8672z"/>
            <path d="m129.01 87.407v0.84072q-0.38125-0.21018-0.7674-0.31283-0.38126-0.10754-0.77229-0.10754-0.87494 0-1.3588 0.55722-0.48391 0.55234-0.48391 1.5544 0 1.002 0.48391 1.5593 0.4839 0.55234 1.3588 0.55234 0.39103 0 0.77229-0.10265 0.38615-0.10753 0.7674-0.31772v0.83095q-0.37636 0.17597-0.78207 0.26395-0.40081 0.08798-0.85538 0.08798-1.2366 0-1.965-0.77718t-0.7283-2.0969q0-1.3393 0.73319-2.1067 0.73808-0.76741 2.0187-0.76741 0.41547 0 0.81139 0.08798 0.39592 0.0831 0.7674 0.25417z"/>
            <path d="m130.55 85.065h0.90427v4.492l2.6835-2.3609h1.1487l-2.9034 2.5613 3.0256 2.9132h-1.1731l-2.7812-2.6737v2.6737h-0.90427z"/>
            <path d="m194.34 85.613v0.96292q-0.56212-0.26884-1.0607-0.40081-0.49857-0.13198-0.96293-0.13198-0.8065 0-1.2464 0.31283-0.43503 0.31283-0.43503 0.8896 0 0.4839 0.28839 0.73319 0.29328 0.2444 1.1047 0.39592l0.59632 0.1222q1.1047 0.21018 1.6277 0.74297 0.52789 0.5279 0.52789 1.4175 0 1.0607-0.71364 1.6081-0.70875 0.54745-2.0823 0.54745-0.51812 0-1.1047-0.11731-0.58166-0.11731-1.2073-0.34704v-1.0167q0.60122 0.33727 1.178 0.50835 0.57677 0.17108 1.134 0.17108 0.84562 0 1.3051-0.33238 0.45947-0.33238 0.45947-0.94826 0-0.53767-0.33239-0.84073-0.32748-0.30305-1.0802-0.45458l-0.60121-0.11731q-1.1047-0.21996-1.5984-0.6892-0.49369-0.46924-0.49369-1.3051 0-0.96781 0.67942-1.525 0.68432-0.55722 1.8819-0.55722 0.51323 0 1.046 0.09287t1.09 0.27861z"/>
            <path d="m197.17 85.642v1.5544h1.8525v0.69898h-1.8525v2.9719q0 0.66965 0.18086 0.86028 0.18575 0.19063 0.74786 0.19063h0.92381v0.75274h-0.92381q-1.0411 0-1.4371-0.38615-0.39592-0.39103-0.39592-1.4175v-2.9719h-0.65987v-0.69898h0.65987v-1.5544z"/>
            <path d="m202.7 89.919q-1.09 0-1.5104 0.24929-0.42037 0.24928-0.42037 0.8505 0 0.47902 0.31283 0.76252 0.31772 0.27861 0.86028 0.27861 0.74785 0 1.1975-0.5279 0.45458-0.53278 0.45458-1.4126v-0.20041zm1.7939-0.37148v3.1234h-0.89938v-0.83095q-0.30794 0.49857-0.76741 0.73808-0.45947 0.23462-1.1242 0.23462-0.84072 0-1.3393-0.46924-0.49368-0.47413-0.49368-1.266 0-0.92382 0.61587-1.3931 0.62077-0.46924 1.8476-0.46924h1.2611v-0.08798q0-0.62077-0.41059-0.95804-0.4057-0.34216-1.1438-0.34216-0.46924 0-0.91405 0.11242-0.4448 0.11242-0.85538 0.33727v-0.83095q0.49368-0.19063 0.95803-0.2835 0.46436-0.09776 0.90427-0.09776 1.1878 0 1.7743 0.61588 0.58655 0.61588 0.58655 1.8672z"/>
            <path d="m209.52 88.037q-0.15153-0.08798-0.33238-0.12709-0.17597-0.04399-0.39103-0.04399-0.76252 0-1.1731 0.49857-0.40569 0.49368-0.40569 1.4224v2.8839h-0.90427v-5.4745h0.90427v0.8505q0.28349-0.49857 0.73807-0.73808 0.45457-0.2444 1.1047-0.2444 0.0929 0 0.2053 0.01467 0.11242 0.0098 0.24928 0.03422l5e-3 0.92382z"/>
            <path d="m211.36 85.642v1.5544h1.8525v0.69898h-1.8525v2.9719q0 0.66965 0.18085 0.86028 0.18574 0.19063 0.74786 0.19063h0.92381v0.75274h-0.92381q-1.0411 0-1.4371-0.38615-0.39592-0.39103-0.39592-1.4175v-2.9719h-0.65987v-0.69898h0.65987v-1.5544z"/>
        </g>
    </g>
    <ellipse class="template l3-base" cx="68.619" cy="114.47" rx="18.699" ry="15.084" fill-opacity="0" stroke-opacity="0"/>
    <ellipse class="template r3-base" cx="203.68" cy="157.39" rx="18.699" ry="15.084" fill-opacity="0" stroke-opacity="0"/>
    <g stroke="#101010">
        <ellipse class="template l3-shadow" cx="68.619" cy="114.47" rx="18.699" ry="15.084" fill="#808080"/>
        <ellipse class="template r3-shadow" cx="203.68" cy="157.39" rx="18.699" ry="15.084" fill="#808080"/>
        <ellipse class="template l3" cx="68.619" cy="114.47" rx="18.699" ry="15.084" fill="#ff0000" fill-opacity=".01"/>
        <ellipse class="template r3" cx="203.68" cy="157.39" rx="18.699" ry="15.084" fill="#ff0000" fill-opacity=".01"/>
        <ellipse cx="161.42" cy="104.94" rx="14.537" ry="11.796" fill="#c0c0c0"/>
    </g>
    <g fill="#ff0000" fill-opacity=".01" stroke="#101010">
        <ellipse class="template ho" cx="161.42" cy="104.94" rx="14.537" ry="11.796"/>
        <path class="template se" d="m130.75 100.18h-5.6318c-2.8501 0-5.1608 2.3102-5.1608 5.16 0 2.8496 2.3107 5.1598 5.1608 5.1598h5.6318c2.8504 0 5.1608-2.3102 5.1608-5.1598 0-2.8498-2.3105-5.16-5.1608-5.16z"/>
        <path class="template st" d="m198.17 100.18h-5.6318c-2.8504 0-5.1608 2.3102-5.1608 5.16 0 2.8496 2.3105 5.1598 5.1608 5.1598h5.6318c2.8501 0 5.1608-2.3102 5.1608-5.1598 0-2.8498-2.3107-5.16-5.1608-5.16z"/>
    </g>
    <path d="m130.23 109.15-6.7534-3.8991 6.7534-3.8991z" fill="#101010" stroke-width="0"/>
    <path d="m192.84 109.15 6.7534-3.8991-6.7534-3.8991z" fill="#101010" stroke-width="0"/>
    <g stroke="#101010">
        <g>
            <path d="m85.932 11.521c-0.66795-1.9639-16.536-0.80338-18.017 1.0387-0.91304 1.4571-0.09651 5.5345-0.86386 10.76-1.2136 8.2679-3.1586 15.811-3.1586 15.811l19.472-1.307s2.4746-3.8554 2.8772-8.6118c0.52805-6.2345 0.88017-14.194-0.30952-17.692z" fill="#c0c0c0"/>
            <path d="m256.24 23.32c-0.76709-5.2258 0.0495-9.3032-0.8636-10.76-1.4805-1.8421-17.349-3.0026-18.017-1.0387-1.1897 3.4975-0.83757 11.457-0.30979 17.692 0.40235 4.7564 2.8772 8.6118 2.8772 8.6118l19.472 1.307c2.6e-4 0-1.9444-7.5436-3.1586-15.811z" fill="#c0c0c0"/>
            <path class="template l2" d="m85.932 11.521c-0.66795-1.9639-16.536-0.80338-18.017 1.0387-0.91304 1.4571-0.09651 5.5345-0.86386 10.76-1.2136 8.2679-3.1586 15.811-3.1586 15.811l19.472-1.307s2.4746-3.8554 2.8772-8.6118c0.52805-6.2345 0.88017-14.194-0.30952-17.692z" fill="#ff0000" fill-opacity=".01"/>
            <path class="template r2" d="m256.24 23.32c-0.76709-5.2258 0.0495-9.3032-0.8636-10.76-1.4805-1.8421-17.349-3.0026-18.017-1.0387-1.1897 3.4975-0.83757 11.457-0.30979 17.692 0.40235 4.7564 2.8772 8.6118 2.8772 8.6118l19.472 1.307c2.6e-4 0-1.9444-7.5436-3.1586-15.811z" fill="#ff0000" fill-opacity=".01"/>
        </g>
        <g fill="#101010" stroke-width=".5">
            <path d="m84.542 11.444c-0.21179 0.55406-0.11986 8.958-0.50917 14.962-0.38931 6.0036-1.5096 11.476-1.5096 11.476l0.84265-0.05361s1.3018-7.9841 1.447-11.33c0.14523-3.3461 0.38857-14.254 0.62477-14.712s0.35696-0.46991 0.35696-0.46991-0.15093-0.14534-0.35804-0.23618c-0.20712-0.09084-0.50332-0.1817-0.50332-0.1817s-0.17949-0.0079-0.39128 0.54619z"/>
            <path d="m238.6 11.402c0.22796 0.49025 0.11986 8.958 0.50916 14.962 0.38932 6.0036 1.6549 11.515 1.6549 11.515l-0.84568-0.04755s-1.4441-8.0295-1.5893-11.376c-0.14523-3.3461-0.38857-14.254-0.62478-14.712-0.23619-0.458-0.2394-0.39999-0.2394-0.39999s0.033-0.04764 0.20184-0.16469c0.16878-0.11705 0.63093-0.26527 0.63093-0.26527s0.0744-0.0018 0.30232 0.48834z"/>
            <path d="m69.996 36.297v-5.2154h0.93269v4.5999h3.4711v0.61546z"/>
            <path d="m77.308 36.297v-4.5999h-2.3221v-0.61546h5.5865v0.61546h-2.3317v4.5999z"/>
            <path d="m243.03 36.297v-5.2154h3.125c0.62819 6e-6 1.1058 0.04685 1.4327 0.14052 0.32691 0.09369 0.58812 0.25911 0.78365 0.49628 0.1955 0.23718 0.29325 0.49925 0.29326 0.78622-1e-5 0.36999-0.16187 0.68187-0.48557 0.93564-0.32372 0.25378-0.82373 0.41505-1.5 0.48383 0.24679 0.08775 0.43429 0.17432 0.5625 0.2597 0.27243 0.185 0.53044 0.41624 0.77403 0.69372l1.226 1.4195h-1.1731l-0.93269-1.0851c-0.27244-0.31306-0.4968-0.55261-0.67307-0.71863-0.17629-0.16602-0.33414-0.28223-0.47356-0.34864s-0.28125-0.11265-0.42548-0.13874c-0.10577-0.0166-0.27885-0.02491-0.51923-0.02491h-1.0817v2.316zm0.93269-2.9136h2.0048c0.42628 3e-6 0.75962-0.0326 1-0.09783 0.24038-0.06522 0.42307-0.16957 0.54808-0.31306 0.12499-0.14348 0.18749-0.29942 0.1875-0.46782-1e-5 -0.24665-0.121-0.44943-0.36298-0.60834-0.242-0.1589-0.62421-0.23835-1.1466-0.23836h-2.2308z"/>
            <path d="m251.92 36.297v-4.5999h-2.3221v-0.61546h5.5865v0.61546h-2.3317v4.5999z"/>
        </g>
        <g>
            <path d="m96.177 41.23c-1.9418-2.0278-16.973-0.09887-30.962 1.8093-12.702 1.733-20.944 5.508-22.885 7.333-1.9415 1.825-5.5845 9.6561-2.9997 9.8875 2.8935 0.25876 14.47-4.8802 27.206-6.1733 14.719-1.4945 27.467 0.84388 28.597-0.0053 2.1574-1.622 2.9855-10.824 1.0437-12.851z" fill="#c0c0c0"/>
            <path d="m280.96 50.372c-1.9418-1.8248-10.183-5.6-22.885-7.333-13.989-1.9081-29.02-3.837-30.962-1.8093-1.9418 2.0275-1.1134 11.229 1.044 12.851 1.1292 0.84914 13.877-1.4892 28.597 0.0053 12.736 1.293 24.313 6.432 27.206 6.1733 2.5848-0.23142-1.0582-8.0627-3-9.8875z" fill="#c0c0c0"/>
            <path class="template l1" d="m96.177 41.23c-1.9418-2.0278-16.973-0.09887-30.962 1.8093-12.702 1.733-20.944 5.508-22.885 7.333-1.9415 1.825-5.5845 9.6561-2.9997 9.8875 2.8935 0.25876 14.47-4.8802 27.206-6.1733 14.719-1.4945 27.467 0.84388 28.597-0.0053 2.1574-1.622 2.9855-10.824 1.0437-12.851z" fill="#ff0000" fill-opacity=".01"/>
            <path class="template r1" d="m280.96 50.372c-1.9418-1.8248-10.183-5.6-22.885-7.333-13.989-1.9081-29.02-3.837-30.962-1.8093-1.9418 2.0275-1.1134 11.229 1.044 12.851 1.1292 0.84914 13.877-1.4892 28.597 0.0053 12.736 1.293 24.313 6.432 27.206 6.1733 2.5848-0.23142-1.0582-8.0627-3-9.8875z" fill="#ff0000" fill-opacity=".01"/>
        </g>
        <g fill="#101010" stroke-width=".5">
            <path d="m74.814 48.817v-5.2154h0.93269v4.5999h3.4711v0.61546z"/>
            <path d="m237.11 48.817v-5.2154h3.125c0.6282 6e-6 1.1058 0.04685 1.4327 0.14052 0.32692 0.09369 0.58813 0.25911 0.78365 0.49628 0.19551 0.23718 0.29326 0.49925 0.29327 0.78622-1e-5 0.36999-0.16187 0.68187-0.48558 0.93564-0.32372 0.25377-0.82372 0.41505-1.5 0.48383 0.24679 0.08775 0.43429 0.17432 0.5625 0.2597 0.27243 0.185 0.53044 0.41624 0.77403 0.69372l1.226 1.4195h-1.1731l-0.93269-1.0851c-0.27244-0.31306-0.4968-0.5526-0.67307-0.71863-0.17629-0.16602-0.33414-0.28223-0.47356-0.34864-0.13943-0.06641-0.28125-0.11265-0.42549-0.13874-0.10577-0.0166-0.27885-0.02491-0.51922-0.02491h-1.0817v2.316zm0.93269-2.9136h2.0048c0.42627 3e-6 0.75961-0.0326 0.99999-0.09783 0.24039-0.06522 0.42308-0.16957 0.54809-0.31306 0.12499-0.14349 0.18748-0.29943 0.18749-0.46782-1e-5 -0.24665-0.12099-0.44944-0.36298-0.60834s-0.6242-0.23835-1.1466-0.23836h-2.2308z"/>
            <path d="m80.295 48.817v-5.2154h2.6442c0.53846 6e-6 0.97035 0.05278 1.2957 0.15831 0.32531 0.10554 0.58012 0.26801 0.76442 0.48739 0.18429 0.21939 0.27644 0.44885 0.27644 0.68839-6e-6 0.22294-0.08174 0.43284-0.24519 0.62969-0.16347 0.19686-0.41026 0.35576-0.74038 0.47671 0.42628 0.0925 0.754 0.25022 0.98317 0.47316 0.22916 0.22294 0.34374 0.4862 0.34375 0.78978-6e-6 0.24429-0.06971 0.47138-0.20913 0.68127-0.13943 0.2099-0.3117 0.37177-0.51683 0.48561s-0.46234 0.19982-0.77163 0.25792c-0.3093 0.05811-0.6883 0.08716-1.137 0.08716zm0.93269-3.0239h1.524c0.41346 4e-6 0.70993-0.02016 0.88942-0.06048 0.23717-0.05217 0.41586-0.13874 0.53606-0.2597 0.12019-0.12095 0.18028-0.27274 0.18029-0.45537-4e-6 -0.17313-0.0561-0.32551-0.16827-0.45715-0.11218-0.13162-0.27244-0.22175-0.48077-0.27037-0.20834-0.04861-0.56571-0.07293-1.0721-0.07293h-1.4087zm0 2.4085h1.7548c0.30128 1e-6 0.51282-0.0083 0.63461-0.02491 0.21474-0.02846 0.39422-0.07589 0.53846-0.1423 0.14423-0.06641 0.26282-0.16305 0.35577-0.28994 0.09295-0.12688 0.13942-0.27334 0.13942-0.43936-5e-6 -0.19448-0.06731-0.36346-0.20192-0.50695-0.13462-0.14349-0.32132-0.24428-0.5601-0.30239-0.23878-0.0581-0.58253-0.08716-1.0312-0.08716h-1.6298z"/>
            <path d="m244.17 48.817v-5.2154h2.6442c0.53846 6e-6 0.97035 0.05278 1.2957 0.15831 0.32532 0.10554 0.58013 0.26801 0.76443 0.48739 0.18428 0.21939 0.27643 0.44885 0.27644 0.68839-1e-5 0.22294-0.0817 0.43284-0.2452 0.62969-0.16346 0.19686-0.41026 0.35576-0.74038 0.47671 0.42628 0.0925 0.754 0.25022 0.98318 0.47316 0.22916 0.22294 0.34374 0.4862 0.34375 0.78978-1e-5 0.24429-0.0697 0.47138-0.20914 0.68127-0.13943 0.2099-0.31171 0.37177-0.51682 0.48561-0.20514 0.11384-0.46235 0.19982-0.77164 0.25792-0.30929 0.05811-0.6883 0.08716-1.137 0.08716zm0.93268-3.0239h1.524c0.41345 4e-6 0.70992-0.02016 0.88942-0.06048 0.23717-0.05217 0.41585-0.13874 0.53605-0.2597 0.12019-0.12095 0.18029-0.27274 0.18029-0.45537 0-0.17313-0.0561-0.32551-0.16827-0.45715-0.11218-0.13162-0.27244-0.22175-0.48077-0.27037-0.20833-0.04861-0.5657-0.07293-1.0721-0.07293h-1.4086zm0 2.4085h1.7548c0.30128 1e-6 0.51281-0.0083 0.6346-0.02491 0.21475-0.02846 0.39424-0.07589 0.53847-0.1423s0.26281-0.16305 0.35577-0.28994c0.0929-0.12688 0.13942-0.27334 0.13942-0.43936 0-0.19448-0.0673-0.36346-0.20192-0.50695-0.13463-0.14349-0.32132-0.24428-0.5601-0.30239-0.23878-0.0581-0.58253-0.08716-1.0312-0.08716h-1.6298z"/>
        </g>
    </g>
    <g stroke-width="0">
        <path d="m289.47 97.57c-1.562-4.6186-7.6393-6.679-13.574-4.6023-5.9342 2.0764-9.4786 7.5042-7.9165 12.123 1.562 4.6186 7.6393 6.6787 13.574 4.6023 5.9345-2.077 9.4791-7.5044 7.9165-12.123z" fill="#ff0000"/>
        <path d="m265.53 120.5c-0.96406-5.229-7.0434-8.5797-13.579-7.4839s-11.052 6.2227-10.088 11.452c0.96406 5.229 7.0434 8.5797 13.579 7.4839 6.5354-1.0956 11.052-6.2227 10.088-11.452z" fill="#00ff00"/>
        <path d="m266.83 81.205c1.064 5.3891-3.2446 10.779-9.6235 12.038-6.3788 1.2594-12.412-2.0884-13.476-7.4776-1.064-5.3891 3.2446-10.779 9.6235-12.038 6.3789-1.2594 12.412 2.0884 13.476 7.4776z" fill="#ffff00"/>
        <path d="m239.89 103.1c-0.42864-5.0343-5.7664-8.7554-11.922-8.3115s-10.798 4.885-10.37 9.9193c0.42891 5.0343 5.7667 8.7554 11.922 8.3115 6.1559-0.44416 10.798-4.885 10.37-9.9193z" fill="#0000ff"/>
        <path class="template ci" d="m289.47 97.57c-1.562-4.6186-7.6393-6.679-13.574-4.6023-5.9342 2.0764-9.4786 7.5042-7.9165 12.123 1.562 4.6186 7.6393 6.6787 13.574 4.6023 5.9345-2.077 9.4791-7.5044 7.9165-12.123z" fill="#0000ff" fill-opacity=".01"/>
    </g>
    <g fill="#ff0000" fill-opacity=".01" stroke-width="0">
        <path class="template cr" d="m265.53 120.5c-0.96406-5.229-7.0434-8.5797-13.579-7.4839s-11.052 6.2227-10.088 11.452c0.96406 5.229 7.0434 8.5797 13.579 7.4839 6.5354-1.0956 11.052-6.2227 10.088-11.452z"/>
        <path class="template tr" d="m266.83 81.205c1.064 5.3891-3.2446 10.779-9.6235 12.038-6.3788 1.2594-12.412-2.0884-13.476-7.4776-1.064-5.3891 3.2446-10.779 9.6235-12.038 6.3789-1.2594 12.412 2.0884 13.476 7.4776z"/>
        <path class="template sq" d="m239.89 103.1c-0.42864-5.0343-5.7664-8.7554-11.922-8.3115s-10.798 4.885-10.37 9.9193c0.42891 5.0343 5.7667 8.7554 11.922 8.3115 6.1559-0.44416 10.798-4.885 10.37-9.9193z"/>
    </g>
    <g fill-opacity="0" stroke="#101010">
        <path d="m274.15 106.56v-10.113l5.2642-0.17121c1.0435-0.03393 1.9158-3.94e-4 2.5466 0.20425 0.63079 0.20466 1.1591 0.4512 1.5165 0.87658 0.35735 0.4254 0.53602 0.87034 0.53603 1.3348-1e-5 0.4323-0.15849 0.8393-0.47543 1.221-0.31698 0.38171-0.82976 0.75832-1.4699 0.99285 0.82656 0.17936 1.4963 0.4167 1.9406 0.84898 0.44435 0.4323 0.70077 0.90853 0.70078 1.4972-1e-5 0.47368-0.16941 0.94826-0.43976 1.3553-0.27035 0.40699-0.6044 0.72087-1.0022 0.94162-0.39775 0.22074-0.93074 0.45593-1.5305 0.56861-0.59974 0.11267-1.3693 0.24388-2.239 0.27172zm1.8085-5.8635 3.0921-0.17121c0.80049-0.0443 1.4108-0.14181 1.7589-0.21999 0.45989-0.10117 0.84061-0.33751 1.0737-0.57206 0.23305-0.23453 0.34958-0.52886 0.34959-0.88298-1e-5 -0.33571-0.10876-0.63118-0.32628-0.88643-0.21753-0.25523-0.56251-0.3615-0.96647-0.45579-0.40398-0.09427-1.1329-0.09719-2.1131-0.03869l-2.8684 0.17121zm0 4.6701 3.5396-0.17121c0.58351-0.0282 1.0286-0.11882 1.2648-0.15102 0.41639-0.0552 0.79867-0.21564 1.0783-0.34441 0.27966-0.12876 0.50961-0.31617 0.68985-0.5622 0.18023-0.24604 0.30458-0.56427 0.30459-0.88618-1e-5 -0.37711-0.16476-0.67053-0.42577-0.94877-0.26104-0.27822-0.65729-0.40519-1.1203-0.51787-0.46301-0.11267-1.165-0.11139-2.0339-0.0663l-3.2972 0.17121z"/>
        <path d="m246.78 127.13 5.2485-10.113h1.9484l5.5934 10.113h-2.0602l-1.5941-3.0628h-5.7146l-1.5009 3.0628zm3.9433-4.1528h4.6332l-1.4263-2.8007c-0.43504-0.85078-0.75822-1.5498-0.96952-2.0971-0.17403 0.64844-0.41951 1.2923-0.73646 1.9315z"/>
        <path d="m254.45 88.67v-4.2838l-5.2671-5.829h2.2001l2.6942 3.049c0.49718 0.57026 0.96019 1.1405 1.389 1.7108 0.41017-0.52886 0.90736-1.1244 1.4916-1.7867l2.6475-2.9732h2.1068l-5.4536 5.829v4.2838z"/>
        <path d="m222.37 109.24 5.2858-5.2703-4.6612-4.8426h2.1534l2.4797 2.5938c0.51583 0.53806 0.8825 0.95197 1.1 1.2417 0.30453-0.3679 0.66499-0.7519 1.0814-1.152l2.7501-2.6834h1.967l-4.801 4.7667 5.1739 5.3462h-2.2374l-3.4399-3.6078c-0.19267-0.20694-0.39155-0.43229-0.59663-0.67604-0.30454 0.36792-0.52205 0.62086-0.65256 0.75882l-3.4306 3.525z"/>
    </g>
</svg>

實作效果可以到在到 https://hkgoldenmra.bitbucket.io/html5-gamepad-detector/index.html 測試
原始碼亦存放到 https://bitbucket.org/hkgoldenmra/hkgoldenmra.bitbucket.io/src/master/html5-gamepad-detector/
可以在 Terminal 輸入
git clone "https://bitbucket.org/hkgoldenmra/hkgoldenmra.bitbucket.io/html5-gamepad-detector.git" --depth=1
下載

在使用 HTML5 Gamepad API 前,在下曾使用 Java 直接存取 /dev/input/js* 的資訊,並在 Terminal 顯示手掣狀態
但之後打算有較豐富的視覺效果,而使用 Java Swing 作圖像界面視示工具,但 Java Swing 在更新手掣的按鈕顯示時有延遲,效果並不流暢
另外, Windows 要存取手掣資訊需要使用 dll 的原生功能,導致 Java 都不能真正跨平台地存取手掣資訊
改用 HTML5 Gamepad API 除了基於跨平台性,還有因為瀏覽器能簡化多執行緒 (Multi-Threading) 的處理,減省很多多執行緒問題

對於 HTML5 Gamepad API ,桌面作業系統中 Firefox 的支援效果及流暢度比 Chrome 較好
但行動作業系統中 Chrome 則比 Firefox 好 (可能是因為 Android 及 Chrome 都是 Google 產品)

參巧資料
W3C 的 Gamepad API 的草擬文件, Gamepad@W3C https://www.w3.org/TR/gamepad/
Gamepad API 基本內容, Gamepad API - Web APIs | MDN https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API
Gamepad API 使用方法, Using the Gamepad API - Web APIs | MDN https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API
顯示基本 Gamepad 按鈕及轉軸資料, HTML5 Gamepad Tester https://html5gamepad.com/

沒有留言 :

張貼留言