feat: add model-aware Controller Studio artwork and topology

This commit is contained in:
Joey Yakimowich-Payne 2026-09-07 12:57:33 -06:00
commit 8451b1eec9
34 changed files with 1898 additions and 4186 deletions

View file

@ -629,6 +629,19 @@ The live playtest, eight-slot catalog, controller-native labels, automatic
active-profile synchronization, and Sets A/B are implemented. Set C remains active-profile synchronization, and Sets A/B are implemented. Set C remains
candidate work; the native Switch-family output plan below is separate. candidate work; the native Switch-family output plan below is separate.
Studio now uses nine supplied SVG assets with model-specific, viewBox-relative
hotspots and sideways Joy-Con source mappings. Live playtest schema 4 adds a
read-only topology byte; the host retains schema 2/3 compatibility. Auto views
follow actual matching-owner pair/solo metadata; explicit previews do not
change firmware topology or highlight another controller as detected hardware.
Rear/rail controls remain off-art and unavailable stored mappings are retained.
Chromium checks covered all assets, ten layout/orientation views, mobile
panning and non-overlapping hotspots, remapping, and simulated live topology
transitions without losing focus or draft data. The connected real Joy-Con pair
reported its paired layout on the flashed firmware. All 293 tests and all five
firmware builds passed. Pair profiles still share the left bank at this
checkpoint; independent pair banks are separate work.
| Priority | Candidate | Intended scope | Dependency or principal risk | | Priority | Candidate | Intended scope | Dependency or principal risk |
|---|---|---|---| |---|---|---|---|
| A1 — Complete | Named and copyable profiles | Profile names and controller aliases use catalog metadata rather than input-profile fields. Copy/export/import preserve the current draft and per-section resets leave other settings unchanged. | Atomic metadata records; no report-path cost. | | A1 — Complete | Named and copyable profiles | Profile names and controller aliases use catalog metadata rather than input-profile fields. Copy/export/import preserve the current draft and per-section resets leave other settings unchanged. | Atomic metadata records; no report-path cost. |

View file

@ -245,7 +245,11 @@ Development USB identities are `CAFE:4010` (XInput), `CAFE:4020` (DInput), and `
Switch 2's **C, GL, GR, Left SL/SR and Right SL/SR** are additional source-only controls. Map each to a normal button or trigger, assign a button-only alternate Shift mapping, or use it in action/macro chords, cancellation and modifiers. Extra mappings default to disabled. The live playtest shows raw extras separately; they are not fictitious output channels on the emulated Switch Pro/XInput controller. Switch 2's **C, GL, GR, Left SL/SR and Right SL/SR** are additional source-only controls. Map each to a normal button or trigger, assign a button-only alternate Shift mapping, or use it in action/macro chords, cancellation and modifiers. Extra mappings default to disabled. The live playtest shows raw extras separately; they are not fictitious output channels on the emulated Switch Pro/XInput controller.
The editor selects Switch Pro, DualSense, or Xbox artwork from the connected controller's USB VID/PID and places each remappable control directly over the matching physical button. Controller artwork is from [AL2009man/Gamepad-Asset-Pack](https://github.com/AL2009man/Gamepad-Asset-Pack) under its MIT license; the bundled license and source revision are recorded beside the assets. Controller Studio uses the supplied lightweight SVGs for Switch 2 Pro, Joy-Con 2 left/right solo and paired layouts, original Switch Pro, DualSense, Xbox, and Wii Remote/Nunchuk views. Hotspots follow the artwork's actual coordinates; solo Joy-Con views rotate with their firmware input mappings. Rear buttons and rails are labeled below the front view rather than drawn in fictitious positions. On narrow screens, pan the diagram or use the **Source control** menu.
**Auto** uses matching-owner live metadata to distinguish a Joy-Con pair from a solo half. **Preview** changes only the editor's diagram and source labels; it does not pair controllers or change saved mappings, and physical highlighting is disabled. Source choices reflect the layout while stored unavailable mappings are retained. Wii orientation is not reported, so horizontal/vertical views require an explicit preview. The current pair still uses the left controller's profile bank; select that owner to edit paired input.
The read-only playtest endpoint (`0x39`) uses schema 4, 56 bytes: byte 55 identifies unspecified (0), Joy-Con 2 left solo (1), right solo (2), pair (3), Wii Remote (4), or Wii Remote + Nunchuk (5). Host tools still read schema 2/54-byte and schema 3/55-byte payloads; older firmware cannot confirm Joy-Con topology. This metadata does not change profile records, input mapping, or rumble.
Profile names and controller aliases are stored as independently checksummed Profile names and controller aliases are stored as independently checksummed
catalog metadata. Runtime profiles use schema 7 and unchanged 384-byte records; schemas 1–6 migrate with extra inputs unmapped and existing settings preserved. Names remain separate. The catalog metadata. Runtime profiles use schema 7 and unchanged 384-byte records; schemas 1–6 migrate with extra inputs unmapped and existing settings preserved. Names remain separate. The

Binary file not shown.

Binary file not shown.

View file

@ -2829,6 +2829,32 @@ void bluepad32_input_backend_playtest_snapshot(
(slot.device->report_parser.set_lightbar_color != nullptr ? 2u : 0u) | (slot.device->report_parser.set_lightbar_color != nullptr ? 2u : 0u) |
(slot.device->report_parser.set_player_leds != nullptr ? 4u : 0u) | (slot.device->report_parser.set_player_leds != nullptr ? 4u : 0u) |
(slot.state.motion_sample_count != 0 ? 8u : 0u); (slot.state.motion_sample_count != 0 ? 8u : 0u);
if (slot.active) {
const int side = joycon_side(slot.device);
if (side != 0) {
out->controller_layout = slot.companion != nullptr
? Bluepad32ControllerLayout::kJoyCon2MergedPair
: side < 0
? Bluepad32ControllerLayout::kJoyCon2LeftSolo
: Bluepad32ControllerLayout::kJoyCon2RightSolo;
} else {
switch (slot.device->controller_subtype) {
case CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL:
case CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL:
case CONTROLLER_SUBTYPE_WIIMOTE_ACCEL:
out->controller_layout =
Bluepad32ControllerLayout::kWiiRemote;
break;
case CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK:
case CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL:
out->controller_layout =
Bluepad32ControllerLayout::kWiiNunchuk;
break;
default:
break;
}
}
}
} }
critical_section_exit(&g_state_lock); critical_section_exit(&g_state_lock);
} }

View file

@ -64,6 +64,15 @@ struct Bluepad32SlotSnapshot {
ControllerState state; ControllerState state;
}; };
enum class Bluepad32ControllerLayout : uint8_t {
kUnspecified = 0,
kJoyCon2LeftSolo = 1,
kJoyCon2RightSolo = 2,
kJoyCon2MergedPair = 3,
kWiiRemote = 4,
kWiiNunchuk = 5,
};
// Side-effect-free raw input snapshot for management telemetry. Unlike the // Side-effect-free raw input snapshot for management telemetry. Unlike the
// report-path snapshot, reading this does not consume motion samples. // report-path snapshot, reading this does not consume motion samples.
struct Bluepad32PlaytestSnapshot { struct Bluepad32PlaytestSnapshot {
@ -74,6 +83,8 @@ struct Bluepad32PlaytestSnapshot {
uint16_t physical_button_mask = 0; uint16_t physical_button_mask = 0;
uint8_t battery = 0; uint8_t battery = 0;
uint8_t capabilities = 0; uint8_t capabilities = 0;
Bluepad32ControllerLayout controller_layout =
Bluepad32ControllerLayout::kUnspecified;
ControllerState state{}; ControllerState state{};
}; };

View file

@ -516,7 +516,9 @@ size_t encode_profile_playtest(
CONTROLLER_IDENTITY_ENCODED_SIZE) || CONTROLLER_IDENTITY_ENCODED_SIZE) ||
snapshot.state.motion_sample_count > snapshot.state.motion_sample_count >
CONTROLLER_MOTION_SAMPLE_CAPACITY || CONTROLLER_MOTION_SAMPLE_CAPACITY ||
(snapshot.state.extra_buttons & 0x80u) != 0) { (snapshot.state.extra_buttons & 0x80u) != 0 ||
static_cast<uint8_t>(snapshot.controller_layout) >
static_cast<uint8_t>(Bluepad32ControllerLayout::kWiiNunchuk)) {
return 0; return 0;
} }
payload[0] = 1; payload[0] = 1;
@ -538,6 +540,7 @@ size_t encode_profile_playtest(
payload[39] = snapshot.battery; payload[39] = snapshot.battery;
payload[40] = snapshot.capabilities; payload[40] = snapshot.capabilities;
payload[54] = snapshot.state.extra_buttons; payload[54] = snapshot.state.extra_buttons;
payload[55] = static_cast<uint8_t>(snapshot.controller_layout);
if (snapshot.state.motion_sample_count != 0) { if (snapshot.state.motion_sample_count != 0) {
payload[0] |= 2; payload[0] |= 2;
const ControllerMotionSample& motion = const ControllerMotionSample& motion =

View file

@ -23,8 +23,8 @@ constexpr size_t kProfileListRowSize =
16 + PROFILE_SERVICE_METADATA_MAX_BYTES + 1; 16 + PROFILE_SERVICE_METADATA_MAX_BYTES + 1;
constexpr size_t kProfileListPayloadSize = constexpr size_t kProfileListPayloadSize =
1 + PROFILE_SERVICE_LIST_CAPACITY * kProfileListRowSize; 1 + PROFILE_SERVICE_LIST_CAPACITY * kProfileListRowSize;
constexpr uint16_t kProfilePlaytestSchemaVersion = 3; constexpr uint16_t kProfilePlaytestSchemaVersion = 4;
constexpr size_t kProfilePlaytestPayloadSize = 55; constexpr size_t kProfilePlaytestPayloadSize = 56;
constexpr size_t kProfileMetadataPayloadSize = constexpr size_t kProfileMetadataPayloadSize =
(CONTROLLER_PROFILE_COUNT + 1) * (CONTROLLER_PROFILE_COUNT + 1) *
(PROFILE_SERVICE_METADATA_MAX_BYTES + 1); (PROFILE_SERVICE_METADATA_MAX_BYTES + 1);

View file

@ -166,8 +166,18 @@ PROFILE_MACRO_REPEAT_MIN = 1
PROFILE_MACRO_REPEAT_MAX = 255 PROFILE_MACRO_REPEAT_MAX = 255
PROFILE_PLAYTEST_LEGACY_SCHEMA_VERSION = 2 PROFILE_PLAYTEST_LEGACY_SCHEMA_VERSION = 2
PROFILE_PLAYTEST_LEGACY_SIZE = 54 PROFILE_PLAYTEST_LEGACY_SIZE = 54
PROFILE_PLAYTEST_SCHEMA_VERSION = 3 PROFILE_PLAYTEST_EXTRA_BUTTON_SCHEMA_VERSION = 3
PROFILE_PLAYTEST_SIZE = 55 PROFILE_PLAYTEST_EXTRA_BUTTON_SIZE = 55
PROFILE_PLAYTEST_SCHEMA_VERSION = 4
PROFILE_PLAYTEST_SIZE = 56
PROFILE_PLAYTEST_LAYOUTS = (
None,
"joycon2-left",
"joycon2-right",
"joycon2-pair",
"wii-remote",
"wii-nunchuk",
)
PROFILE_PLAYTEST_SLOT_COUNT = 4 PROFILE_PLAYTEST_SLOT_COUNT = 4
PROFILE_METADATA_SCHEMA_VERSION = 1 PROFILE_METADATA_SCHEMA_VERSION = 1
PROFILE_METADATA_MAX_BYTES = 31 PROFILE_METADATA_MAX_BYTES = 31
@ -900,6 +910,7 @@ class ProfilePlaytest:
capabilities: int capabilities: int
motion: tuple[int, int, int, int, int, int] | None motion: tuple[int, int, int, int, int, int] | None
extra_buttons: int = 0 extra_buttons: int = 0
layout: str | None = None
def to_json_object(self) -> dict[str, Any]: def to_json_object(self) -> dict[str, Any]:
return { return {
@ -917,6 +928,7 @@ class ProfilePlaytest:
if self.identity is not None if self.identity is not None
else None else None
), ),
"layout": self.layout,
"buttons": _button_mask_to_json(self.button_mask), "buttons": _button_mask_to_json(self.button_mask),
"extra_buttons": [ "extra_buttons": [
name for index, name in enumerate(EXTRA_BUTTONS) name for index, name in enumerate(EXTRA_BUTTONS)
@ -3629,12 +3641,16 @@ def parse_profile_playtest(envelope: Envelope) -> ProfilePlaytest:
_raise_status(envelope) _raise_status(envelope)
expected_size = { expected_size = {
PROFILE_PLAYTEST_LEGACY_SCHEMA_VERSION: PROFILE_PLAYTEST_LEGACY_SIZE, PROFILE_PLAYTEST_LEGACY_SCHEMA_VERSION: PROFILE_PLAYTEST_LEGACY_SIZE,
PROFILE_PLAYTEST_EXTRA_BUTTON_SCHEMA_VERSION: PROFILE_PLAYTEST_EXTRA_BUTTON_SIZE,
PROFILE_PLAYTEST_SCHEMA_VERSION: PROFILE_PLAYTEST_SIZE, PROFILE_PLAYTEST_SCHEMA_VERSION: PROFILE_PLAYTEST_SIZE,
}.get(envelope.schema_version) }.get(envelope.schema_version)
if len(envelope.payload) != expected_size: if len(envelope.payload) != expected_size:
raise ConfigManagerError("invalid profile playtest payload") raise ConfigManagerError("invalid profile playtest payload")
payload = envelope.payload payload = envelope.payload
extra_buttons = payload[54] if envelope.schema_version == PROFILE_PLAYTEST_SCHEMA_VERSION else 0 extra_buttons = payload[54] if len(payload) >= PROFILE_PLAYTEST_EXTRA_BUTTON_SIZE else 0
layout_code = payload[55] if envelope.schema_version == PROFILE_PLAYTEST_SCHEMA_VERSION else 0
if layout_code >= len(PROFILE_PLAYTEST_LAYOUTS):
raise ConfigManagerError("invalid playtest controller layout")
if extra_buttons & ~0x7F: if extra_buttons & ~0x7F:
raise ConfigManagerError("invalid playtest extra buttons") raise ConfigManagerError("invalid playtest extra buttons")
flags = payload[0] flags = payload[0]
@ -3687,6 +3703,7 @@ def parse_profile_playtest(envelope: Envelope) -> ProfilePlaytest:
capabilities=payload[40], capabilities=payload[40],
motion=motion_values if has_motion else None, motion=motion_values if has_motion else None,
extra_buttons=extra_buttons, extra_buttons=extra_buttons,
layout=PROFILE_PLAYTEST_LAYOUTS[layout_code],
) )

View file

@ -24,17 +24,35 @@ _ASSET_TYPES = {
"text/javascript; charset=utf-8", "text/javascript; charset=utf-8",
), ),
"/style.css": ("profile_editor.css", "text/css; charset=utf-8"), "/style.css": ("profile_editor.css", "text/css; charset=utf-8"),
"/assets/controller-switch-pro.svg": ( "/assets/controller_layouts.js": (
"assets/controller-switch-pro.svg", "controller_layouts.js", "text/javascript; charset=utf-8",
"image/svg+xml",
), ),
"/assets/controller-dualsense.svg": ( "/assets/switch-pro-controller-simple.svg": (
"assets/controller-dualsense.svg", "assets/switch-pro-controller-simple.svg", "image/svg+xml",
"image/svg+xml",
), ),
"/assets/controller-xbox.svg": ( "/assets/switch-2-pro-controller-simple.svg": (
"assets/controller-xbox.svg", "assets/switch-2-pro-controller-simple.svg", "image/svg+xml",
"image/svg+xml", ),
"/assets/switch-2-joycon-left.svg": (
"assets/switch-2-joycon-left.svg", "image/svg+xml",
),
"/assets/switch-2-joycon-single.svg": (
"assets/switch-2-joycon-single.svg", "image/svg+xml",
),
"/assets/switch-2-joycons-connected.svg": (
"assets/switch-2-joycons-connected.svg", "image/svg+xml",
),
"/assets/ps5-dualsense-simple.svg": (
"assets/ps5-dualsense-simple.svg", "image/svg+xml",
),
"/assets/xbox-controller-simple.svg": (
"assets/xbox-controller-simple.svg", "image/svg+xml",
),
"/assets/wii-remote-simple.svg": (
"assets/wii-remote-simple.svg", "image/svg+xml",
),
"/assets/wii-remote-nunchuk-simple.svg": (
"assets/wii-remote-nunchuk-simple.svg", "image/svg+xml",
), ),
} }
@ -110,34 +128,92 @@ _CONTROL_LABELS = {
"left_stick": "L3", "left_stick": "L3",
"right_stick": "R3", "right_stick": "R3",
}, },
"wii": {
"north": "B",
"east": "2",
"south": "1",
"west": "A",
"select": "Minus",
"start": "Plus",
"system": "Home",
},
} }
def _controller_presentation( def _controller_presentation(
identity: config_manager.ControllerIdentity, identity: config_manager.ControllerIdentity,
layout: str | None = None,
) -> dict[str, str]: ) -> dict[str, str]:
live_layouts = {
"joycon2-left": ("Nintendo Joy-Con 2 (L)", "switch", {0x2067}),
"joycon2-right": ("Nintendo Joy-Con 2 (R)", "switch", {0x2066}),
"joycon2-pair": ("Nintendo Joy-Con 2 pair", "switch", {0x2067, 0x2066}),
"wii-remote": ("Nintendo Wii Remote", "wii", {0x0306, 0x0330}),
"wii-nunchuk": ("Nintendo Wii Remote + Nunchuk", "wii", {0x0306, 0x0330}),
}
live = live_layouts.get(layout)
if live is not None and (
identity.is_global_fallback
or identity.vendor_id == 0x057E and identity.product_id in live[2]
):
return {"model": live[0], "style": live[1], "layout": cast(str, layout)}
if identity.is_global_fallback: if identity.is_global_fallback:
return {"model": "Generic controller", "style": "generic"} return {"model": "Generic controller", "style": "generic", "layout": "generic"}
known = { known = {
(0x057E, 0x2009): ("Nintendo Switch Pro Controller", "switch"), (0x057E, 0x2009): ("Nintendo Switch Pro Controller", "switch", "switch-pro"),
(0x057E, 0x2069): ("Nintendo Switch 2 Pro Controller", "switch"), (0x057E, 0x2069): ("Nintendo Switch 2 Pro Controller", "switch", "switch2-pro"),
(0x057E, 0x2067): ("Nintendo Joy-Con 2 (L)", "switch"), (0x057E, 0x2067): ("Nintendo Joy-Con 2 (L)", "switch", "joycon2-left"),
(0x057E, 0x2066): ("Nintendo Joy-Con 2 (R)", "switch"), (0x057E, 0x2066): ("Nintendo Joy-Con 2 (R)", "switch", "joycon2-right"),
(0x054C, 0x0CE6): ("Sony DualSense", "playstation"), (0x054C, 0x0CE6): ("Sony DualSense", "playstation", "dualsense"),
(0x054C, 0x09CC): ("Sony DualShock 4", "playstation"), (0x054C, 0x0DF2): ("Sony DualSense Edge", "playstation", "dualsense"),
(0x054C, 0x09CC): ("Sony DualShock 4", "playstation", "generic"),
# VID/PID cannot distinguish Wii extensions or Wii U Pro from Remote Plus.
(0x057E, 0x0306): ("Nintendo Wii controller", "generic", "generic"),
(0x057E, 0x0330): ("Nintendo Wii / Wii U controller", "generic", "generic"),
} }
exact = known.get((identity.vendor_id, identity.product_id)) exact = known.get((identity.vendor_id, identity.product_id))
if exact is not None: if exact is not None:
model, style = exact model, style, model_layout = exact
return {"model": model, "style": style} return {"model": model, "style": style, "layout": model_layout}
if identity.vendor_id == 0x054C: if identity.vendor_id == 0x054C:
return {"model": "Sony controller", "style": "playstation"} return {"model": "Sony controller", "style": "playstation", "layout": "generic"}
if identity.vendor_id == 0x045E: if identity.vendor_id == 0x045E:
return {"model": "Xbox controller", "style": "xbox"} return {"model": "Xbox controller", "style": "xbox", "layout": "xbox"}
if identity.vendor_id == 0x2DC8: if identity.vendor_id == 0x2DC8:
return {"model": "8BitDo controller", "style": "switch"} return {"model": "8BitDo controller", "style": "switch", "layout": "generic"}
return {"model": "Connected controller", "style": "generic"} return {"model": "Connected controller", "style": "generic", "layout": "generic"}
def _source_controls(controller: dict[str, str]) -> list[str]:
layout = controller["layout"]
if layout == "switch2-pro":
controls = config_manager.OUTPUT_CONTROLS + ("c", "gl", "gr")
elif layout == "joycon2-pair":
controls = config_manager.OUTPUT_CONTROLS + (
"c", "left_sl", "left_sr", "right_sl", "right_sr",
)
elif layout in {"joycon2-left", "joycon2-right"}:
controls = (
"south", "east", "west", "north", "left_shoulder",
"right_shoulder", "left_stick",
) + (
("select", "capture", "left_trigger", "left_sl", "left_sr")
if layout == "joycon2-left"
else ("start", "system", "right_trigger", "c", "right_sl", "right_sr")
)
elif layout in {"wii-remote", "wii-nunchuk"}:
controls = (
"south", "east", "west", "north", "select", "start", "system",
"dpad_up", "dpad_down", "dpad_left", "dpad_right",
) + (
("left_shoulder", "right_shoulder") if layout == "wii-nunchuk" else ()
)
elif layout in {"switch-pro", "dualsense", "xbox"} or controller["style"] == "playstation":
controls = config_manager.OUTPUT_CONTROLS
else:
controls = config_manager.LOGICAL_CONTROLS
return list(controls)
def _controller_label( def _controller_label(
@ -446,28 +522,21 @@ class ProfileEditorHandler(BaseHTTPRequestHandler):
def _list_profiles(self) -> dict[str, Any]: def _list_profiles(self) -> dict[str, Any]:
device = self.profile_server.find_device() device = self.profile_server.find_device()
entries = config_manager.list_profiles(device) entries = config_manager.list_profiles(device)
return { identities = []
"identities": [ for index, entry in enumerate(entries):
{ controller = _controller_presentation(entry.identity)
"index": index, source_controls = _source_controls(controller)
"label": _controller_label(entry.identity, entry.alias), identities.append({
"key": entry.identity.to_bytes().hex(), "index": index,
"active_profile": entry.active_profile_index + 1, "label": _controller_label(entry.identity, entry.alias),
"controller": _controller_presentation(entry.identity), "key": entry.identity.to_bytes().hex(),
"alias": entry.alias, "active_profile": entry.active_profile_index + 1,
"modifier_controls": list( "controller": controller,
config_manager.LOGICAL_CONTROLS "alias": entry.alias,
if entry.identity.vendor_id == 0x057E "source_controls": source_controls,
and entry.identity.product_id in {0x2069, 0x2067, 0x2066} "modifier_controls": source_controls,
else config_manager.OUTPUT_CONTROLS })
if _controller_presentation(entry.identity)["style"] return {"identities": identities}
in {"xbox", "playstation"}
else config_manager.LOGICAL_BUTTONS + config_manager.EXTRA_BUTTONS
),
}
for index, entry in enumerate(entries)
]
}
def _read_profile(self, identity_index: int, profile_index: int) -> dict[str, Any]: def _read_profile(self, identity_index: int, profile_index: int) -> dict[str, Any]:
device = self.profile_server.find_device() device = self.profile_server.find_device()
@ -495,10 +564,11 @@ class ProfileEditorHandler(BaseHTTPRequestHandler):
else None else None
) )
result["controller"] = ( result["controller"] = (
_controller_presentation(playtest.identity) _controller_presentation(playtest.identity, playtest.layout)
if playtest.identity is not None if playtest.connected and playtest.identity is not None
else None else _controller_presentation(identity)
) )
result["source_controls"] = _source_controls(result["controller"])
result["label"] = ( result["label"] = (
_controller_label(playtest.identity) _controller_label(playtest.identity)
if playtest.identity is not None if playtest.identity is not None

View file

@ -1,35 +0,0 @@
Controller artwork source
=========================
The following files are redistributed from AL2009man/Gamepad-Asset-Pack
revision a6d1113b0b3b2438bbbcb08a33f9a25eb2391d7c:
- controller-dualsense.svg
- controller-switch-pro.svg
- controller-xbox.svg
Source: https://github.com/AL2009man/Gamepad-Asset-Pack
Copyright (c) 2024 Al. Lopez
License: MIT
MIT License
Copyright (c) 2024 Al. Lopez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 205 KiB

View file

@ -1,463 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="419.12717mm"
height="304.54602mm"
viewBox="0 0 419.12717 304.54603"
version="1.1"
id="svg5"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
sodipodi:docname="Switch Pro Controller VSCView.svg"
inkscape:export-filename="C:\Users\AL200\OneDrive\Pictures\AL2009man Work Projects\VSCView - Controller Theme Layout\VSCView - Controller Theme (Ver. 1.20)\Nintendo Switch Controller Images\Switch Pro Controller\Templates\Switch Pro Controller Overlay.png"
inkscape:export-xdpi="89.994171"
inkscape:export-ydpi="89.994171"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.5"
inkscape:cx="709"
inkscape:cy="694"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg5" />
<defs
id="defs2">
<inkscape:path-effect
effect="bspline"
id="path-effect11354"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect11187"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect41470"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
</defs>
<g
id="layer2"
inkscape:label="Nintendo Switch Pro Controller"
style="display:inline"
transform="translate(-80.073102,14.620127)">
<g
id="g10592"
inkscape:label="Digital Triggers"
style="display:inline">
<path
id="path1458"
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:#211e1e;stroke-width:8.31496;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
inkscape:label="ZR Trigger"
d="m 1193.8594,3.9244459 c -30.6291,0.049759 -51.9558,10.8927201 -82.25,24.1087571 l -30.9864,15.619141 v 0 c 60.6006,-2.534721 177.2868,6.012391 206.2911,17.507812 17.5526,6.941643 47.521,19.514293 73.8007,43.001954 C 1310.1862,40.291377 1254.6354,3.8257017 1193.8594,3.9244459 Z"
sodipodi:nodetypes="scccccs"
inkscape:export-filename="C:\Users\AL200\OneDrive\Pictures\AL2009man Work Projects\VSCView - Controller Theme Layout\VSCView - Controller Theme (Ver. 1.20)\Nintendo Switch Controller Images\Switch Pro Controller\Theme SVG\Theme Assets\Active Presses\Button Color\NSwitchPro_ZR.png"
inkscape:export-xdpi="89.874435"
inkscape:export-ydpi="89.874435"
transform="matrix(0.26458333,0,0,0.26458333,80.073102,-14.558417)" />
<path
id="path8519"
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:#211e1e;stroke-width:8.31496;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
inkscape:label="ZL Trigger"
d="M 388.70312,3.9244459 C 327.84634,3.8255797 272.22886,40.385003 221.64648,104.41406 247.97833,80.800743 278.04726,68.120999 295.64844,61.160156 324.65272,49.664735 441.33889,41.117623 501.93945,43.652344 v 0 L 470.95312,28.033203 C 440.65896,14.817164 419.33216,3.9742049 388.70312,3.9244459 Z"
transform="matrix(0.26458333,0,0,0.26458333,80.073102,-14.558417)"
sodipodi:nodetypes="scccccs"
inkscape:export-filename="C:\Users\AL200\OneDrive\Pictures\AL2009man Work Projects\VSCView - Controller Theme Layout\VSCView - Controller Theme (Ver. 1.20)\Nintendo Switch Controller Images\Switch Pro Controller\Theme SVG\Theme Assets\Active Presses\Button Color\NSwitchPro_ZL.png"
inkscape:export-xdpi="90.144424"
inkscape:export-ydpi="90.144424" />
</g>
<g
id="g3226"
style="display:inline"
inkscape:label="Controller Body"
inkscape:export-filename="C:\Users\AL200\OneDrive\Pictures\AL2009~1\VSCVIE~1\VSCVIE~1.20)\NINTEN~1\SWITCH~1\TEMPLA~1\SWITCH~4.PNG"
inkscape:export-xdpi="89.994171"
inkscape:export-ydpi="89.994171">
<g
id="g20999"
inkscape:label="Shoulders"
style="display:inline">
<path
style="display:inline;fill:#3a3b40;fill-opacity:1;fill-rule:nonzero;stroke:#211e1e;stroke-width:2.8;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 278.583,-2.464337 c -28.76946,0 -85.94915,1.52074839 -111.02858,7.9960404 -25.07936,6.4590456 269.26062,6.4590456 244.18129,0 C 386.63998,-0.94358861 329.46023,-2.464337 300.69077,-2.464337 Z m 0,0"
id="path1675-3"
inkscape:label="Shoulder Top" />
<g
id="g3215"
inkscape:label="Bumpers"
style="display:inline">
<path
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:#211e1e;stroke-width:2.2;stroke-linejoin:round;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 349.00293,5.0036016 20.78687,-9.8062225 c 16.0339,-0.670645 43.10528,3.3847582 50.77933,6.426255 7.69035,3.041349 24.39363,10.1544219 30.7451,27.7326099"
id="path5183"
sodipodi:nodetypes="cccc"
inkscape:label="R Bumper" />
<path
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:#211e1e;stroke-width:2.2;stroke-linejoin:round;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 229.86272,5.0036016 -20.78687,-9.8062225 c -16.0339,-0.670645 -43.10528,3.3847582 -50.77933,6.426255 -7.69035,3.041349 -24.39363,10.1544219 -30.7451,27.7326099"
id="path2365"
sodipodi:nodetypes="cccc"
inkscape:label="L Bumper" />
</g>
<path
style="display:inline;fill:#3a3b40;fill-opacity:1;fill-rule:nonzero;stroke:#211e1e;stroke-width:2.8;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 278.583,3.9619915 c -28.76946,0 -85.94915,1.5206749 -111.02858,7.9795715 -25.07936,6.475292 -47.95454,23.203229 -47.95454,23.203229 -1.22458,2.387321 -2.92887,6.475293 -2.92887,6.475293 27.03871,4.692961 105.59767,6.818626 168.0186,6.93317 54.12641,0.118702 146.98623,3.335721 180.52342,0.01486 0,0 -3.30226,-9.876446 -5.131,-13.441106 0,0 -23.26699,-16.711541 -48.34632,-23.186833 C 386.63998,5.4812083 329.46023,3.9604598 300.69077,3.9604598 Z"
id="path1685-5"
sodipodi:nodetypes="cccccccccc"
inkscape:label="Shoulder Middle" />
</g>
<path
id="path9077"
style="color:#000000;display:inline;fill:#303136;fill-opacity:1;stroke-width:6.04724;stroke-linejoin:round;stroke-miterlimit:9.53846;-inkscape-stroke:none"
d="m 1492.3419,363.6276 c 0,0 -120.2829,187.36702 -214.886,314.43124 -47.6216,63.96202 -90.2553,112.22577 -99.0594,118.48022 27.4793,18.41386 45.2809,61.44345 90.4551,155.73242 68.1907,142.33002 119.3493,163.89862 176.1855,159.57232 56.836,-4.3279 127.8652,-70.4537 132.1231,-182.56255 2.9038,-76.49795 -44.0083,-367.63608 -84.8183,-565.65365 z"
transform="matrix(0.26458333,0,0,0.26458333,80.574244,-5.6765164)"
inkscape:label="Secondary Color - Right Grip"
sodipodi:nodetypes="cscsccc" />
<path
id="path9067"
style="color:#000000;display:inline;fill:#303136;fill-opacity:1;stroke-width:6.04724;stroke-linejoin:round;stroke-miterlimit:9.53846;-inkscape-stroke:none"
d="M 402.42219,795.85127 C 399.38105,793.06819 336.76422,723.08734 320.52137,703.52495 258.47949,628.80361 167.18978,498.80406 115.64482,412.79566 95.419032,379.04672 87.37911,367.31405 87.37911,367.31405 46.574325,565.32969 0.25264151,852.79352 3.15625,929.28125 c 4.2576,112.10885 75.287003,178.23635 132.12305,182.56255 56.836,4.3277 107.99474,-17.2423 176.18554,-159.57232 45.17551,-94.29154 63.47682,-138.00721 90.95735,-156.42021 z"
transform="matrix(0.26458333,0,0,0.26458333,80.574244,-5.6765164)"
inkscape:label="Secondary Color - Left Grip"
sodipodi:nodetypes="csscccsc" />
<path
id="path9073"
style="color:#000000;display:inline;opacity:1;fill:#3a3b40;fill-opacity:1;stroke-width:6.04724;stroke-linejoin:round;stroke-miterlimit:9.53846;-inkscape-stroke:none"
d="m 747.63867,60.839844 c -110.83321,0 -331.01859,5.748832 -427.6582,30.222656 -96.57774,24.41158 -184.70117,87.69531 -184.70117,87.69531 -11.01001,20.95955 -28.77243,92.5631 -47.282585,181.69038 0.873431,0.0997 0.707604,2.43957 1.16188,3.1922 0.715225,1.38029 27.588795,50.1741 66.836665,108.25343 66.51882,98.43512 163.05602,227.8697 177.48473,243.46334 32.4081,35.02463 70.21689,79.2567 70.90251,79.45725 12.98151,-7.07235 30.71466,-10.76753 51.98078,-10.76753 h 667.58982 c 21.3568,0 38.2517,2.93993 51.2679,10.08307 1.7345,-0.43644 25.6031,-26.33704 59.4122,-64.27553 13.2152,-14.82933 41.5543,-49.99609 41.5543,-49.99609 0,0 59.0253,-77.54759 113.8892,-157.48009 54.4807,-79.37419 104.8128,-161.13504 101.9565,-161.74543 -18.5107,-89.12547 -35.9671,-160.91574 -46.9961,-181.875 0,0 -88.0614,-63.28373 -184.7012,-87.69531 C 1163.7579,66.588676 943.50977,60.839844 832.73828,60.839844 Z"
transform="matrix(0.26458333,0,0,0.26458333,80.574244,-5.6765164)"
inkscape:label="Primary Color - Front"
sodipodi:nodetypes="sccccsscsscscscccss" />
<path
id="path44935"
style="color:#000000;display:inline;fill:#211e1e;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:9.53846;-inkscape-stroke:none"
d="m 278.38666,9.1209533 c -29.36903,0 -87.49413,1.4566687 -113.4706,8.0351677 -25.88784,6.543567 -49.30656,23.408391 -49.30655,23.408391 -0.16443,0.117573 -0.29839,0.272371 -0.39274,0.451134 a 0.50005001,0.50005001 0 0 0 -0.002,0 c -1.28805,2.452052 -2.7102,6.774486 -4.34548,12.705167 -1.63548,5.931445 -3.44791,13.429921 -5.35367,22.011576 -3.81155,17.163289 -7.995508,38.658521 -11.851476,60.523441 -3.855972,21.86493 -7.382584,44.0963 -9.876917,62.7326 -2.494304,18.63606 -3.96839,33.60292 -3.677812,41.25743 1.146538,30.19005 20.208895,48.33477 36.160065,49.54891 7.71368,0.58736 15.31861,-0.64567 23.09006,-6.83782 7.76932,-6.19044 15.73229,-17.19509 24.79694,-36.11511 9.02811,-18.84372 13.9548,-29.97353 18.73994,-36.1425 2.39403,-3.08636 4.69,-4.92631 7.52564,-6.06474 2.83367,-1.13765 6.27904,-1.56631 10.89804,-1.56631 h 176.63315 c 4.61903,0 8.06488,0.42866 10.89856,1.56631 2.83565,1.13843 5.1311,2.97839 7.52512,6.06474 4.78521,6.16902 9.71394,17.29881 18.74201,36.1425 9.06463,18.92 17.02604,29.92469 24.79538,36.11511 7.77147,6.1921 15.37586,7.42499 23.08955,6.83782 15.95108,-1.21461 35.01344,-19.35886 36.16006,-49.54891 0.29056,-7.6544 -1.18272,-22.62137 -3.67574,-41.25743 -2.49301,-18.63629 -6.01778,-40.86765 -9.87273,-62.7326 -3.85495,-21.86495 -8.03917,-43.362164 -11.85148,-60.525507 -1.90614,-8.581679 -3.7191,-16.077965 -5.35574,-22.00951 -1.6364,-5.93072 -3.05906,-10.252647 -4.34961,-12.705167 a 0.50005001,0.50005001 0 0 0 -0.002,0 c -0.0944,-0.178761 -0.22833,-0.333558 -0.39275,-0.451134 -0.004,-0.0028 -23.40438,-16.865401 -49.30654,-23.408391 -25.9608,-6.578724 -84.10235,-8.0351677 -113.45509,-8.0351677 z m 0,2.5998407 h 22.51593 c 29.26218,0 87.6711,1.584111 112.8143,7.955071 5.3e-4,1.38e-4 0.001,-1.38e-4 0.002,0 a 0.50005001,0.50005001 0 0 0 0,0.0021 c 24.79081,6.262258 47.36709,22.251704 48.14021,22.802741 1.05212,2.078752 2.46999,6.229985 4.04317,11.931573 1.61812,5.864421 3.42424,13.330539 5.32422,21.884452 3.79997,17.107826 7.97854,38.573109 11.82822,60.408199 3.84968,21.83505 7.36883,44.04288 9.85521,62.62925 2.48634,18.58613 3.92719,33.63042 3.65456,40.81249 -1.10646,29.13294 -19.64105,45.98175 -33.75608,47.05656 -7.32887,0.55788 -14.00144,-0.48695 -21.27105,-6.2792 -7.26742,-5.79049 -15.09723,-16.4695 -24.0745,-35.20716 -9.01368,-18.81362 -13.85919,-29.94323 -19.0314,-36.6112 -2.58745,-3.33571 -5.31452,-5.56064 -8.61291,-6.88485 -3.29649,-1.32345 -7.08838,-1.75183 -11.86543,-1.75183 h -176.6325 c -4.77702,0 -8.56894,0.42838 -11.86543,1.75183 -3.29844,1.32423 -6.02399,3.54923 -8.61136,6.88485 -5.17218,6.66792 -10.01976,17.79755 -19.03346,36.6112 -8.97728,18.73766 -16.80657,29.41662 -24.07399,35.20716 -7.2696,5.7923 -13.94274,6.83726 -21.27156,6.2792 -14.11497,-1.07438 -32.649174,-17.92362 -33.755566,-47.05656 -0.27264,-7.18195 1.16849,-22.22637 3.656108,-40.81249 2.487652,-18.58637 6.008639,-40.79417 9.859345,-62.62925 3.850713,-21.83509 8.031053,-43.300284 11.830283,-60.408199 1.8996,-8.553937 3.70513,-16.019931 5.32216,-21.884452 1.5725,-5.70302 2.99039,-9.854922 4.04109,-11.93364 0.78309,-0.557723 23.367,-16.539757 48.1366,-22.800674 a 0.50005001,0.50005001 0 0 0 0,-5.3e-4 0.50005001,0.50005001 0 0 0 0,-5.29e-4 0.50005001,0.50005001 0 0 0 0,-5.29e-4 0.50005001,0.50005001 0 0 0 0,-5.29e-4 c 5.3e-4,-1.38e-4 10e-4,1.38e-4 0.002,0 25.16012,-6.37119 83.55126,-7.955071 112.8298,-7.955071 z"
inkscape:label="Controller Outline" />
<path
id="path15766"
style="color:#000000;display:inline;opacity:1;fill:#211e1e;fill-opacity:1;stroke-width:0.2;stroke-linecap:square;stroke-miterlimit:9.53846;-inkscape-stroke:none"
d="m 103.54984,89.758219 c -0.46441,0.240657 -0.80478,0.65855 -0.56351,1.122682 8.42782,16.264539 56.93552,89.848909 83.91219,114.452959 0.38624,0.35299 0.64705,0.10389 0.99937,-0.28288 0.35301,-0.3863 0.66878,-0.77094 0.28172,-1.12321 -26.41372,-24.09041 -75.45466,-98.34021 -83.58167,-114.024199 -0.1369,-0.265665 -0.95889,-0.191562 -1.0481,-0.145352 z m 371.93828,-0.254497 -0.10335,0.0041 c -0.33848,0.01543 -0.64484,0.210875 -0.80099,0.511598 -8.10964,15.68127 -57.07626,89.81787 -83.48999,113.90829 -0.38727,0.35208 -0.30372,0.78502 0.0491,1.17149 0.35227,0.38708 0.84561,0.58765 1.23193,0.2346 26.97647,-24.60363 75.48404,-98.17394 83.89668,-114.441079 0.23913,-0.46419 0.0557,-1.03981 -0.40823,-1.279509 -0.11581,-0.05996 -0.2459,-0.07335 -0.37518,-0.08217 z"
sodipodi:nodetypes="ccccccccccccccccc"
inkscape:label="Outline Color Blend" />
<g
id="g20546"
style="display:inline"
inkscape:label="Joystick Outline">
<g
id="g20740"
style="display:inline"
inkscape:label="Analog Stick Outline Body - Right">
<path
style="display:inline;fill:none;stroke:#211e1e;stroke-width:1.6;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 377.83935,140.67905 c 0,18.82089 -15.0705,34.07712 -33.66779,34.07712 -18.5973,0 -33.66772,-15.25623 -33.66772,-34.07712 0,-18.82088 15.07042,-34.07703 33.66772,-34.07703 18.59729,0 33.66779,15.25615 33.66779,34.07703 z m 0,0"
id="path1711-8"
inkscape:label="Outline #2" />
<path
style="display:inline;fill:#2c2c2c;fill-opacity:1;fill-rule:nonzero;stroke:#211e1e;stroke-width:1.6;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 370.998,140.67905 c 0,14.99458 -12.00083,27.16027 -26.82644,27.16027 -14.82554,0 -26.82652,-12.16569 -26.82652,-27.16027 0,-14.99457 12.00098,-27.14386 26.82652,-27.14386 14.82561,0 26.82644,12.14929 26.82644,27.14386"
id="path1713-1"
inkscape:label="Outline #1" />
</g>
<g
id="g20744"
style="display:inline"
inkscape:label="Analog Stick Outline Body - Left">
<path
style="display:inline;fill:none;stroke:#211e1e;stroke-width:1.6;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 207.91654,82.385032 c 0,18.820888 -15.07045,34.077108 -33.66774,34.077108 -18.59729,0 -33.66783,-15.25622 -33.66783,-34.077108 0,-18.82096 15.07054,-34.077037 33.66783,-34.077037 18.59729,0 33.66774,15.256077 33.66774,34.077037 z m 0,0"
id="path1695-1"
inkscape:label="Outline #2" />
<path
style="display:inline;fill:#2c2c2c;fill-opacity:1;stroke:#211e1e;stroke-width:1.6;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 201.07528,82.385032 c 0,14.99457 -12.01729,27.143938 -26.82648,27.143938 -14.82564,0 -26.82648,-12.149368 -26.82648,-27.143938 0,-14.99457 12.00084,-27.160262 26.82648,-27.160262 14.80919,0 26.82648,12.165692 26.82648,27.160262 z m 0,0"
id="path1699-1"
inkscape:label="Outline #1" />
</g>
</g>
<path
id="path14321"
style="display:inline;fill:#c7c7c7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.297881"
d="m 287.64024,16.987688 v 5.61e-4 h -7.18474 c -4.55541,0 -8.25167,3.691333 -8.25167,8.246746 V 40.91088 c 0,4.555458 3.69626,8.246199 8.25167,8.246199 h 7.18474 c 0.1302,0 0.23893,-0.103795 0.23893,-0.233995 V 17.222229 c 0,-0.1302 -0.10873,-0.234541 -0.23893,-0.234541 z m 3.19589,0 c -0.11456,0 -0.20769,0.09424 -0.20769,0.208785 V 48.92307 c 0,0.1302 0.10381,0.233996 0.23399,0.233996 h 5.26348 c 4.5554,0 8.24674,-3.690743 8.24674,-8.246201 l -5.6e-4,-15.676431 c 0,-4.555413 -3.6908,-8.246746 -8.2462,-8.246746 z m -10.38063,2.74928 h 4.67 v 26.670816 h -4.67 c -1.47336,0 -2.85306,-0.572603 -3.88911,-1.613838 -1.04123,-1.036003 -1.61384,-2.415507 -1.61384,-3.883628 V 25.234434 c 0,-1.46812 0.57261,-2.847867 1.61384,-3.889106 1.03605,-1.036003 2.41575,-1.60836 3.88911,-1.60836 z m -0.14084,3.878693 c -1.67116,0 -3.02437,1.353716 -3.02437,3.02492 0,1.665928 1.35321,3.024922 3.02437,3.024922 1.67121,0 3.02492,-1.358994 3.02492,-3.024922 0,-1.671204 -1.35371,-3.02492 -3.02492,-3.02492 z m 16.90998,8.03851 c 1.67114,0 3.02492,1.35895 3.02492,3.024919 0,1.671206 -1.35378,3.024373 -3.02492,3.024373 -1.67121,0 -3.02492,-1.353167 -3.02492,-3.024373 0,-1.665969 1.35371,-3.024919 3.02492,-3.024919 z m 12.75507,22.209604 c -1.21304,0 -2.19689,0.984148 -2.19689,2.202384 0,1.218235 0.98385,2.207317 2.19689,2.207317 1.21824,0 2.2024,-0.989082 2.2024,-2.207317 0,-1.218236 -0.98416,-2.202384 -2.2024,-2.202384 z m -45.34587,0.135355 v 4.138989 h 0.95791 v -2.519672 l 1.87412,2.519672 h 0.90091 V 53.99913 h -0.95789 v 2.478023 l -1.85331,-2.478023 z m 6.62744,0 v 4.138989 h 1.03625 V 53.99913 Z m 3.93075,0 v 4.138989 h 0.95296 v -2.519672 l 1.87907,2.519672 h 0.89541 V 53.99913 h -0.95788 v 2.478023 l -1.85332,-2.478023 z m 6.46084,0 v 0.994606 h 1.28066 v 3.144383 h 1.00501 v -3.144383 h 1.24943 V 53.99913 Z m 6.34631,0 v 4.138989 h 3.25398 v -0.885007 h -2.2961 V 56.46181 h 2.12402 v -0.869667 h -2.12402 v -0.712937 h 2.2961 V 53.99913 Z m 6.45589,0 v 4.138989 h 0.95242 v -2.519672 l 1.87961,2.519672 h 0.89542 V 53.99913 h -0.95788 v 2.478023 l -1.85332,-2.478023 z m 6.99679,0 v 4.138989 h 1.72891 c 1.17657,0 2.12896,-0.926652 2.12896,-2.07196 0,-1.140206 -0.95239,-2.067029 -2.12896,-2.067029 z m 8.52785,0.895421 c 0.64557,0 1.17162,0.526025 1.17162,1.171608 0,0.650864 -0.52609,1.171061 -1.17162,1.171061 -0.64035,0 -1.16613,-0.520331 -1.16613,-1.171061 0,-0.645537 0.52578,-1.171608 1.16613,-1.171608 z m -7.55956,0.08822 h 0.80226 c 0.5987,0 1.08284,0.484688 1.08284,1.083382 0,0.603927 -0.48414,1.087766 -1.08284,1.087766 h -0.80226 z m -2.38431,4.784525 c -2.30114,0 -4.16475,1.864213 -4.16475,4.165296 0,2.295982 1.86361,4.159815 4.16475,4.159815 1.47859,0 2.78567,-0.775887 3.51975,-1.942089 l -1.4478,-1.259831 c -0.36465,0.739317 -1.13484,1.249423 -2.03031,1.249423 -1.24427,0 -2.25389,-0.98908 -2.25389,-2.207314 0,-1.223426 1.00962,-2.212795 2.25389,-2.212795 0.89547,0 1.66585,0.51015 2.03031,1.249422 l 1.4478,-1.259834 C 302.82151,60.543141 301.51443,59.7673 300.03584,59.7673 Z m -32.22638,0.140836 c -1.61911,0 -3.49289,0.78078 -3.49289,2.40514 0,1.327587 0.95255,1.936495 3.11313,2.311434 1.57748,0.270735 2.24558,0.406117 2.22322,0.921724 -0.0156,0.535922 -0.68738,0.83295 -1.86921,0.83295 -1.67115,0 -2.7438,-0.760066 -2.7438,-0.760066 l -0.84828,1.536022 c 0,0 1.45729,0.87953 3.48249,0.87953 2.76447,0 3.80581,-1.093181 3.80581,-2.540496 0,-1.213046 -0.63007,-1.993651 -3.56634,-2.509259 -0.87986,-0.156193 -1.66074,-0.286757 -1.71796,-0.734309 -0.0312,-0.21333 0.11992,-0.697596 1.6922,-0.697596 1.17659,0 2.37938,0.562239 2.37938,0.562239 l 0.64006,-1.561778 c 0,0 -1.43703,-0.645535 -3.09781,-0.645535 z m 4.25901,0.10905 2.21225,7.809447 h 2.02045 l 1.30148,-4.597113 1.29108,4.597113 h 2.02538 l 2.22813,-7.809447 h -1.93167 l -1.35354,5.09195 -1.4889,-5.09195 h -1.54644 l -1.47848,5.071127 -1.34862,-5.071127 z m 12.67178,0 v 7.809447 h 1.9525 v -7.809447 z m 3.55045,0 v 1.874684 h 2.41062 v 5.934763 h 1.89497 v -5.934763 h 2.35308 v -1.874684 z m 16.7379,0 v 7.814926 h 1.83249 v -3.009573 h 3.24905 v 3.009577 h 1.83741 v -7.814926 h -1.83741 v 2.967929 h -3.24905 v -2.967929 z"
inkscape:label="Nintendo Switch Logo" />
</g>
<g
id="g10760"
inkscape:label="Analog Joysticks"
style="display:inline">
<g
id="g2495"
inkscape:label="Right Joystick"
style="display:inline;stroke-width:0.901068"
transform="matrix(1.1098172,0,0,1.1097717,-37.796036,-15.434424)"
inkscape:export-filename="C:\Users\AL200\OneDrive\Pictures\AL2009~1\VSCVIE~1\VSCVIE~1.20)\NINTEN~1\SWITCH~1\THEMES~1\THEMEA~1\ACTIVE~1\BUTTON~1\NSWITC~4.PNG"
inkscape:export-xdpi="90.207504"
inkscape:export-ydpi="90.207504">
<path
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.44171;stroke-miterlimit:9.53846;stroke-dasharray:none"
d="m 366.06709,140.67905 c 0,12.23121 -9.79667,22.15669 -21.89553,22.15669 -12.08246,0 -21.89553,-9.92548 -21.89553,-22.15669 0,-12.23112 9.81307,-22.1566 21.89553,-22.1566 12.09886,0 21.89553,9.92548 21.89553,22.1566"
id="path1717-9"
inkscape:label="Color" />
<path
id="path1719-5"
style="color:#000000;fill:#211e1e;stroke-width:3.77953;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:9.53846;-inkscape-stroke:none"
d="m 998.16797,457.98242 c -52.54376,0 -95.24609,43.2182 -95.24609,96.33985 0,53.12169 42.70233,96.33203 95.24609,96.33203 52.61003,0 95.24803,-43.21336 95.24803,-96.33203 0,-53.11863 -42.638,-96.33985 -95.24803,-96.33985 z m 0,6.80664 c 48.88923,0 88.43943,40.04719 88.43943,89.53321 0,49.48601 -39.5502,89.5332 -88.43943,89.5332 -48.81792,0 -88.4375,-40.05021 -88.4375,-89.5332 0,-49.483 39.61958,-89.53321 88.4375,-89.53321 z m -0.0312,19.52344 c -38.2086,0 -69.17774,31.42482 -69.17774,70.00977 0,38.64299 30.9725,70.00195 69.17774,70.00195 38.20533,0 69.24223,-31.35568 69.24223,-70.00195 0,-38.58827 -31.0336,-70.00977 -69.24223,-70.00977 z m 0,6.04492 c 34.89833,0 63.19733,28.61127 63.19733,63.96485 0,35.4169 -28.2957,63.95703 -63.19733,63.95703 -34.90163,0 -63.13281,-28.53685 -63.13281,-63.95703 0,-35.35685 28.23449,-63.96485 63.13281,-63.96485 z"
transform="matrix(0.23840262,0,0,0.2384124,106.2058,8.5222802)"
inkscape:label="Outline" />
</g>
<g
id="g2489"
inkscape:label="Left Joystick"
style="display:inline;stroke-width:0.901069"
transform="matrix(1.1098161,0,0,1.1097704,-19.135317,-9.0434377)"
inkscape:export-filename="C:\Users\AL200\OneDrive\Pictures\AL2009~1\VSCVIE~1\VSCVIE~1.20)\NINTEN~1\SWITCH~1\THEMES~1\THEMEA~1\ACTIVE~1\BUTTON~1\NSWITC~3.PNG"
inkscape:export-xdpi="87.630684"
inkscape:export-ydpi="87.630684">
<path
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.44171;stroke-miterlimit:9.53846;stroke-dasharray:none"
d="m 196.14423,82.385032 c 0,12.231124 -9.81296,22.156678 -21.89551,22.156678 -12.09884,0 -21.89543,-9.925554 -21.89543,-22.156678 0,-12.231198 9.79659,-22.156755 21.89543,-22.156755 12.08255,0 21.89551,9.925557 21.89551,22.156755"
id="path1701-7"
inkscape:label="Color" />
<path
id="path1707-6"
style="display:inline;fill:none;stroke:#211e1e;stroke-width:1.44171;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 190.02859,82.385027 c 0,8.814462 -7.05807,15.969873 -15.77265,15.969873 -8.71458,0 -15.78701,-7.155411 -15.78701,-15.969873 0,-8.814527 7.07243,-15.969941 15.78701,-15.969941 8.71458,0 15.77265,7.155414 15.77265,15.969941 z m 0,0 m 6.11564,5e-6 c 0,12.231124 -9.81296,22.156678 -21.89551,22.156678 -12.09884,0 -21.89543,-9.925554 -21.89543,-22.156678 0,-12.231198 9.79659,-22.156755 21.89543,-22.156755 12.08255,0 21.89551,9.925557 21.89551,22.156755 z m 0,0"
inkscape:label="Outline" />
</g>
</g>
<g
id="g1427"
inkscape:label="Face Buttons"
style="display:inline">
<g
id="g10427"
inkscape:label="X Button">
<path
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.6;stroke-miterlimit:9.53846;stroke-dasharray:none"
d="m 414.49511,53.671306 c 0,-8.323055 -6.66169,-15.043682 -14.87451,-15.043682 -8.21289,0 -14.85826,6.720627 -14.85826,15.043682 0,8.306659 6.64537,15.043533 14.85826,15.043533 8.21282,0 14.87451,-6.736874 14.87451,-15.043533"
id="path1737-4"
inkscape:label="Color" />
<path
style="display:inline;fill:none;stroke:#211e1e;stroke-width:1.6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 414.49511,53.671306 c 0,-8.323055 -6.66169,-15.043607 -14.87451,-15.043607 -8.21289,0 -14.85826,6.720552 -14.85826,15.043607 0,8.306659 6.64537,15.043681 14.85826,15.043681 8.21282,0 14.87451,-6.737022 14.87451,-15.043681 z m 0,0"
id="path1739-4"
inkscape:label="Outline" />
<path
id="path5694"
style="color:#000000;display:inline;fill:#ffffff;stroke-width:0.499999;stroke-miterlimit:10;-inkscape-stroke:none"
d="m 395.50194,48.189305 0.23257,0.379307 3.00548,4.923729 -3.45093,5.644618 h 1.79472 l 2.5528,-4.183724 2.5373,4.183724 h 1.79473 l -3.45094,-5.644618 3.23805,-5.303036 h -1.77715 l -2.34199,3.842145 -2.34145,-3.842145 z"
inkscape:label="Text" />
</g>
<g
id="g10432"
inkscape:label="Y Button">
<path
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.6;stroke-miterlimit:9.53846;stroke-dasharray:none"
d="m 367.25896,67.68476 c -8.21282,0 -14.87451,6.720553 -14.87451,15.043607 0,8.306734 6.66169,15.043682 14.87451,15.043682 8.21289,0 14.87459,-6.736948 14.87459,-15.043682 0,-8.323054 -6.6617,-15.043607 -14.87459,-15.043607"
id="path1733-7"
inkscape:label="Color" />
<path
style="display:inline;fill:none;stroke:#211e1e;stroke-width:1.6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 367.25904,67.68476 c -8.2129,0 -14.87459,6.720553 -14.87459,15.043607 0,8.306734 6.66169,15.043682 14.87459,15.043682 8.21281,0 14.87451,-6.736948 14.87451,-15.043682 0,-8.323054 -6.6617,-15.043607 -14.87451,-15.043607 z m 0,0"
id="path1735-4"
inkscape:label="Outline" />
<path
id="path5704"
style="color:#000000;display:inline;fill:#ffffff;stroke-width:0.499999;stroke-miterlimit:10;-inkscape-stroke:none"
d="m 363.00989,77.245929 0.23048,0.379306 3.29488,5.402255 v 5.167643 h 1.51154 V 83.02749 l 3.47472,-5.781561 h -1.77562 l -2.47272,4.086055 -2.50372,-4.086055 z"
inkscape:label="Text" />
</g>
<g
id="g10422"
inkscape:label="A Button">
<path
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.6;stroke-miterlimit:9.53846;stroke-dasharray:none"
d="m 432.60263,97.772049 c 8.21289,0 14.87458,-6.736948 14.87458,-15.043682 0,-8.30666 -6.66169,-15.060003 -14.87458,-15.060003 -8.21282,0 -14.87459,6.753343 -14.87459,15.060003 0,8.306734 6.66177,15.043682 14.87459,15.043682"
id="path1725-4"
inkscape:label="Color" />
<path
style="display:inline;fill:none;stroke:#211e1e;stroke-width:1.6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 432.60263,97.772049 c 8.21281,0 14.87451,-6.736948 14.87451,-15.043682 0,-8.30666 -6.6617,-15.060003 -14.87451,-15.060003 -8.2129,0 -14.87467,6.753343 -14.87467,15.060003 0,8.306734 6.66177,15.043682 14.87467,15.043682 z m 0,0"
id="path1727-2"
inkscape:label="Outline" />
<path
id="path5684"
style="color:#000000;display:inline;fill:#ffffff;stroke-width:0.499999;stroke-miterlimit:10;-inkscape-stroke:none"
d="m 432.3457,76.134888 -5.11546,10.947135 h 1.71101 l 1.59991,-3.433384 h 4.14031 l 1.61541,3.433384 h 1.67587 l -5.04672,-10.947135 h -0.1602 z m 0.28731,3.064412 1.35911,2.935221 h -2.74196 z"
inkscape:label="Text" />
</g>
<g
id="g10417"
inkscape:label="B Button"
style="display:inline">
<path
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.6;stroke-miterlimit:9.53846;stroke-dasharray:none"
d="m 384.76234,111.19683 c 0,8.30666 6.64537,15.04361 14.85826,15.04361 8.21282,0 14.87451,-6.73695 14.87451,-15.04361 0,-8.32305 -6.66169,-15.059925 -14.87451,-15.059925 -8.21289,0 -14.85826,6.736875 -14.85826,15.059925"
id="path1729-5"
inkscape:label="Color" />
<path
style="display:inline;fill:none;stroke:#211e1e;stroke-width:1.6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 384.76234,111.19683 c 0,8.30666 6.64544,15.04361 14.85826,15.04361 8.21282,0 14.87451,-6.73695 14.87451,-15.04361 0,-8.32305 -6.66169,-15.060074 -14.87451,-15.060074 -8.21282,0 -14.85826,6.737024 -14.85826,15.060074 z m 0,0"
id="path1731-8"
inkscape:label="Outline" />
<path
id="path5714"
style="color:#000000;display:inline;fill:#ffffff;stroke-width:0.499999;stroke-miterlimit:10;-inkscape-stroke:none"
d="m 399.47225,105.71448 -2.29081,5.3e-4 v 0.24959 10.69961 h 3.09181 c 1.07363,0 1.95181,-0.30075 2.62154,-0.93586 h 0.003 v -0.002 c 0.64646,-0.62979 0.97462,-1.41132 0.97462,-2.30425 0,-0.44095 -0.0871,-0.86836 -0.26718,-1.24799 l -0.003,-0.002 c -0.17555,-0.38581 -0.41021,-0.71541 -0.72864,-0.99839 l -0.004,-0.002 -0.003,-0.002 c -0.24373,-0.20363 -0.61497,-0.35596 -0.98599,-0.51004 0.29816,-0.22146 0.60555,-0.43885 0.77721,-0.74207 0.25247,-0.41527 0.3788,-0.88916 0.3788,-1.4025 0,-0.53531 -0.14348,-1.04004 -0.41031,-1.48622 l -0.003,-0.004 -0.004,-0.006 c -0.28318,-0.42562 -0.66442,-0.77237 -1.15033,-0.98857 h -0.003 c -0.49593,-0.22406 -1.16128,-0.31626 -1.99625,-0.31626 z m -0.77668,1.52962 h 0.94101 c 0.7048,0 1.16608,0.13228 1.42214,0.34985 l 0.006,0.006 0.006,0.004 c 0.28122,0.21413 0.40618,0.49457 0.40618,0.91209 0,0.31298 -0.0794,0.58368 -0.24804,0.83768 -0.18725,0.25995 -0.40392,0.43401 -0.70126,0.54725 -0.27373,0.10158 -0.79496,0.17932 -1.47846,0.17932 h -0.35346 z m 0.33742,4.39456 c 0.85844,0 1.47833,0.0404 1.76993,0.11111 0.4972,0.12048 0.85116,0.33801 1.10897,0.63872 l 0.003,0.004 0.003,0.002 c 0.27053,0.2999 0.39274,0.6386 0.39274,1.04283 0,0.32553 -0.096,0.61375 -0.28318,0.88884 v 0.002 l -0.003,0.002 c -0.18621,0.28793 -0.41764,0.47979 -0.73432,0.60927 h -0.003 -0.003 c -0.28475,0.12139 -0.8182,0.20877 -1.55083,0.20877 h -1.0392 v -3.5078 z"
inkscape:label="Text" />
</g>
</g>
<g
id="g1458"
inkscape:label="Home"
style="display:inline">
<g
id="g3754"
inkscape:label="LED"
style="display:inline">
<path
id="path1749-3"
style="display:none;mix-blend-mode:lighten;fill:#b0b0b0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.04724;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
inkscape:label="OFF"
d="m 902.57812,287.30469 c -25.42484,0 -46.0996,20.88841 -46.0996,46.66015 0,25.77147 20.67476,46.59961 46.0996,46.59961 25.48661,0 46.09766,-20.82814 46.09766,-46.59961 0,-25.77174 -20.61105,-46.66015 -46.09766,-46.66015 z m 0,12.66992 c 18.5751,0 33.57032,15.20215 33.57032,33.99023 0,18.72584 -14.99522,33.99219 -33.57032,33.99219 -18.51337,0 -33.57226,-15.26635 -33.57226,-33.99219 0,-18.78808 15.05889,-33.99023 33.57226,-33.99023 z"
transform="matrix(0.26458333,0,0,0.26458333,80.072992,-5.9766386)" />
<path
id="path4088"
style="display:inline;mix-blend-mode:lighten;fill:#2a4fff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.04724;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
inkscape:label="ON"
d="m 902.57812,287.30469 c -25.42484,0 -46.0996,20.88841 -46.0996,46.66015 0,25.77147 20.67476,46.59961 46.0996,46.59961 25.48661,0 46.09766,-20.82814 46.09766,-46.59961 0,-25.77174 -20.61105,-46.66015 -46.09766,-46.66015 z m 0,12.66992 c 18.5751,0 33.57032,15.20215 33.57032,33.99023 0,18.72584 -14.99522,33.99219 -33.57032,33.99219 -18.51337,0 -33.57226,-15.26635 -33.57226,-33.99219 0,-18.78808 15.05889,-33.99023 33.57226,-33.99023 z"
transform="matrix(0.26458333,0,0,0.26458333,80.072992,-5.9766386)" />
</g>
<path
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.6;stroke-miterlimit:9.53846;stroke-dasharray:none"
d="m 318.87991,73.39148 c 4.91466,0 8.88234,4.022539 8.88234,8.993552 0,4.954544 -3.96768,8.993478 -8.88234,8.993478 -4.89833,0 -8.88233,-4.038934 -8.88233,-8.993478 0,-4.971013 3.984,-8.993552 8.88233,-8.993552"
id="path4084"
inkscape:label="Color" />
<path
id="path1751-4"
style="color:#000000;display:inline;fill:#211e1e;stroke-width:3.77953;stroke-miterlimit:9.53846;-inkscape-stroke:none"
d="m 900.68555,283.15039 c -27.08126,0 -49.125,22.29152 -49.125,49.67969 0,27.38843 22.04865,49.62109 49.125,49.62109 27.13383,0 49.11914,-22.23751 49.11914,-49.62109 0,-27.38333 -21.98043,-49.67969 -49.11914,-49.67969 z m 0,6.04492 c 23.83442,0 43.07422,19.47576 43.07422,43.63477 0,24.1593 -19.23489,43.57617 -43.07422,43.57617 -23.77391,0 -43.08008,-19.42172 -43.08008,-43.57617 0,-24.15416 19.31108,-43.63477 43.08008,-43.63477 z m 0,6.6211 c -20.1687,0 -36.59766,16.61035 -36.59766,37.01367 0,20.34695 16.42224,37.01367 36.59766,37.01367 20.23162,0 36.59375,-16.67339 36.59375,-37.01367 0,-20.39667 -16.36882,-37.01367 -36.59375,-37.01367 z m 0,6.04687 c 16.92521,0 30.54687,13.7879 30.54687,30.9668 0,17.11191 -13.62835,30.96679 -30.54687,30.9668 -16.85129,0 -30.55274,-13.86156 -30.55274,-30.9668 0,-17.17225 13.69472,-30.9668 30.55274,-30.9668 z"
transform="matrix(0.26458333,0,0,0.26458333,80.574244,-5.6765164)"
inkscape:label="Outline" />
<path
style="display:inline;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.297882"
d="m 320.56433,84.51897 c 0,0.245744 -0.0984,0.344162 -0.33691,0.344162 h -2.69787 c -0.23909,0 -0.33751,-0.09842 -0.33751,-0.344162 v -2.016361 c 0,-0.239102 0.0984,-0.336311 0.33751,-0.336311 h 2.69787 c 0.23185,0 0.32302,0.09117 0.33691,0.323631 z m 3.56907,-3.280941 c -1.67914,-1.426277 -3.35829,-2.852432 -5.03743,-4.278708 -0.15457,-0.140683 -0.28076,-0.140683 -0.43533,0 -1.67214,1.426276 -3.35829,2.852431 -5.03744,4.278708 -0.29525,0.252988 -0.1757,0.589904 0.22462,0.589904 h 0.65336 c 0.23186,0 0.33027,0.09842 0.33027,0.337519 v 4.376944 c 0,0.245743 0.0984,0.343557 0.34416,0.343557 h 7.41215 c 0.2391,0 0.33751,-0.09842 0.33751,-0.343557 v -4.376944 c 0,-0.239101 0.0984,-0.337519 0.33028,-0.337519 h 0.65336 c 0.40032,0 0.52711,-0.336916 0.22462,-0.589904"
id="path14283"
inkscape:label="Icon" />
</g>
<g
id="g1451"
inkscape:label="Capture"
style="display:inline">
<path
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.564261"
d="m 255.83852,90.838877 c -2.02465,0 -3.67381,-1.667934 -3.67381,-3.711846 v -8.813649 c 0,-2.043912 1.64916,-3.695451 3.67381,-3.695451 h 8.57199 c 2.02473,0 3.65741,1.651539 3.65741,3.695451 v 8.813649 c 0,2.043912 -1.63268,3.711846 -3.65741,3.711846 z m 0,0"
id="path1759-9"
inkscape:label="Color" />
<path
style="display:inline;fill:#211e1e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.564261"
d="m 264.41051,73.751285 h -8.58839 c -2.46546,0 -4.50643,2.06038 -4.50643,4.562097 v 8.813649 c 0,2.518112 2.04097,4.562172 4.50643,4.562172 h 8.58839 c 2.48186,0 4.50651,-2.04406 4.50651,-4.562172 v -8.813649 c 0,-2.501717 -2.02465,-4.562097 -4.50651,-4.562097 m 0,1.716971 c 1.5512,0 2.80845,1.27534 2.80845,2.845126 v 8.813649 c 0,1.569787 -1.25725,2.861595 -2.80845,2.861595 h -8.58839 c -1.55112,0 -2.8083,-1.291808 -2.8083,-2.861595 v -8.813649 c 0,-1.569786 1.25718,-2.845126 2.8083,-2.845126 h 8.58839"
id="path1761-8"
inkscape:label="Outline" />
<path
style="display:inline;fill:#545759;fill-opacity:1;stroke:#211e1e;stroke-width:1.6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 265.42287,82.728367 c 0,2.959595 -2.3675,5.363385 -5.30656,5.363385 -2.92265,0 -5.30641,-2.40379 -5.30641,-5.363385 0,-2.97599 2.38376,-5.379707 5.30641,-5.379707 2.93906,0 5.30656,2.403717 5.30656,5.379707 z m 0,0"
id="path1765-8"
inkscape:label="Symbol" />
</g>
<g
id="g1445"
inkscape:label="Minus"
style="display:inline">
<path
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.6;stroke-miterlimit:9.53846;stroke-dasharray:none"
d="m 247.75631,51.005934 c 0,5.281706 -4.21264,9.549506 -9.43749,9.549506 -5.19221,0 -9.4211,-4.2678 -9.4211,-9.549506 0,-5.281557 4.22889,-9.549357 9.4211,-9.549357 5.22485,0 9.43749,4.2678 9.43749,9.549357"
id="path1767-5"
inkscape:label="Color" />
<path
style="display:inline;fill:none;stroke:#211e1e;stroke-width:1.6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 247.75624,51.005934 c 0,5.281706 -4.21257,9.549506 -9.43742,9.549506 -5.19221,0 -9.4211,-4.2678 -9.4211,-9.549506 0,-5.281557 4.22889,-9.549357 9.4211,-9.549357 5.22485,0 9.43742,4.2678 9.43742,9.549357 z m 0,0"
id="path1769-6"
inkscape:label="Outline" />
<path
id="path6584"
style="color:#000000;display:inline;fill:#000000;fill-opacity:1;stroke-width:0.499999;stroke-miterlimit:9.53846;-inkscape-stroke:none"
d="m 233.05657,50.134924 v 0.249598 1.492414 h 10.55646 v -1.742012 z"
inkscape:label="Symbol" />
</g>
<g
id="g1440"
inkscape:label="Plus"
style="display:inline">
<path
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.6;stroke-miterlimit:9.53846;stroke-dasharray:none"
d="m 350.06585,51.005934 c 0,5.085409 -4.08195,9.222343 -9.11089,9.222343 -5.02887,0 -9.11083,-4.136934 -9.11083,-9.222343 0,-5.085408 4.08196,-9.222343 9.11083,-9.222343 5.02894,0 9.11089,4.136935 9.11089,9.222343"
id="path1773-8"
inkscape:label="Color" />
<path
style="display:inline;fill:none;stroke:#211e1e;stroke-width:1.6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:9.53846;stroke-dasharray:none;stroke-opacity:1"
d="m 350.06585,51.005934 c 0,5.085409 -4.08195,9.222492 -9.11082,9.222492 -5.02894,0 -9.1109,-4.137083 -9.1109,-9.222492 0,-5.085408 4.08196,-9.222343 9.1109,-9.222343 5.02887,0 9.11082,4.136935 9.11082,9.222343 z m 0,0"
id="path1775-8"
inkscape:label="Outline" />
<path
id="path6498"
style="color:#000000;display:inline;fill:#000000;fill-opacity:1;stroke-width:0.499999;stroke-miterlimit:9.53846;-inkscape-stroke:none"
d="m 340.03506,45.866968 v 4.218863 h -4.1951 v 1.840198 h 4.1951 v 4.202843 h 1.82211 v -4.202843 h 4.19766 v -1.840198 h -4.19766 v -4.218863 z"
inkscape:label="Symbol" />
</g>
<g
id="g1435"
inkscape:label="D-PAD"
style="display:inline">
<path
style="display:inline;fill:#686b6e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.564261"
d="m 215.9498,171.45317 c -0.47339,0 -0.84908,-0.37613 -0.84908,-0.85085 v -19.19701 h -18.95656 c -0.47338,0 -0.849,-0.37613 -0.849,-0.85077 v -19.73672 c 0,-0.47482 0.37557,-0.86621 0.849,-0.86621 h 18.95656 V 110.771 c 0,-0.47482 0.37558,-0.86628 0.84908,-0.86628 h 19.49534 c 0.47345,0 0.8491,0.39322 0.8491,0.86628 v 19.18061 h 18.97279 c 0.45713,0 0.84903,0.39248 0.84903,0.86621 v 19.73672 c 0,0.47482 -0.39182,0.85077 -0.84903,0.85077 h -18.97279 v 19.19701 c 0,0.47482 -0.3755,0.85085 -0.8491,0.85085 z m 0,0"
id="path1779-7"
inkscape:label="Color" />
<path
style="display:inline;fill:#211e1e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.6;stroke-miterlimit:9.53846;stroke-dasharray:none"
d="M 235.44514,109.05477 H 215.9498 c -0.93071,0 -1.69808,0.76895 -1.69808,1.7169 v 18.33036 h -18.10749 c -0.94692,0 -1.69807,0.76887 -1.69807,1.7169 v 19.73665 c 0,0.9481 0.75107,1.71689 1.69807,1.71689 h 18.10749 v 18.33037 c 0,0.9481 0.76745,1.70058 1.69808,1.70058 h 19.49534 c 0.94698,0 1.69805,-0.75226 1.69805,-1.70058 v -18.33037 h 18.12384 c 0.93066,0 1.68173,-0.76887 1.68173,-1.71689 v -19.73665 c 0,-0.9481 -0.75107,-1.7169 -1.68173,-1.7169 h -18.12384 v -18.33036 c 0,-0.9481 -0.75107,-1.7169 -1.69805,-1.7169 m 0,1.7169 v 20.04726 h 19.82189 v 19.73665 h -19.82189 v 20.04726 H 215.9498 v -20.04726 h -19.80557 v -19.73665 h 19.80557 v -20.04726 h 19.49534"
id="path1781-2"
inkscape:label="Outline" />
<path
id="path1783-2"
style="display:inline;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.13264"
d="m 548.21875,463.86523 c -0.77142,0 -1.54272,0.51065 -2.12891,1.53125 l -3.26953,5.68555 c -1.17267,2.03928 -3.0854,5.43727 -4.25781,7.47852 l -3.27148,5.74804 c -1.17238,2.04084 -0.18743,3.70899 2.16015,3.70899 h 21.53711 c 2.3562,0 3.33257,-1.66774 2.16016,-3.70899 l -3.27149,-5.74804 c -1.1721,-2.03955 -3.08544,-5.43727 -4.25781,-7.47852 l -3.27148,-5.68555 c -0.58606,-1.0206 -1.35749,-1.53125 -2.12891,-1.53125 z m -67.85352,77.11524 c -0.48532,0.006 -1.02874,0.16564 -1.60156,0.49609 l -5.61523,3.27539 c -2.03543,1.23364 -5.36999,3.15108 -7.40625,4.32617 l -5.61524,3.33594 c -2.03539,1.17468 -2.03539,3.08881 0,4.26367 l 5.61524,3.33789 c 2.03539,1.17495 5.36999,3.09052 7.40625,4.32813 l 5.61523,3.27344 c 2.03539,1.17494 3.70313,0.16819 3.70313,-2.16211 v -21.81641 c 0,-1.68879 -0.86129,-2.67406 -2.10157,-2.6582 z m 135.65821,0.002 c -1.22227,-0.0113 -2.05078,0.97942 -2.05078,2.6543 v 21.8164 c 0,2.34966 1.60436,3.33507 3.64062,2.16016 l 5.67774,-3.27344 c 2.03625,-1.23647 5.3689,-3.15299 7.34375,-4.32812 l 5.67773,-3.33399 c 2.03626,-1.17498 2.03626,-3.09053 0,-4.26562 l -5.67773,-3.33594 c -1.97458,-1.17468 -5.3075,-3.09048 -7.34375,-4.32812 l -5.67774,-3.27344 c -0.5727,-0.33046 -1.11156,-0.48775 -1.58984,-0.49219 z m -78.57227,80.0918 c -2.35619,0 -3.33256,1.66774 -2.16015,3.70898 l 3.27148,5.74805 c 1.17237,2.03928 3.08544,5.37551 4.25781,7.47852 l 3.26953,5.68554 c 1.17241,2.04125 3.08544,2.04125 4.25782,0 l 3.27148,-5.68554 c 1.17241,-2.10123 3.08544,-5.43727 4.25781,-7.47852 l 3.27149,-5.74805 c 1.17267,-2.04079 0.18742,-3.70898 -2.16016,-3.70898 z"
transform="matrix(0.26458333,0,0,0.26458333,80.574244,-5.6765164)"
inkscape:label="Symbols" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 101 KiB

View file

@ -0,0 +1,117 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" height="548" viewBox="0 0 800 548" role="img" aria-labelledby="title description">
<title id="title">PS5 DualSense wireless controller — white</title>
<desc id="description">Front-view white-and-black PlayStation 5 DualSense controller, with symmetrical analog sticks, a touchpad with blue edge lights, directional buttons, triangle circle cross square controls, speaker, PlayStation and microphone buttons. Transparent background. Lightweight vector shapes and simple gradients; no filters, textures, fonts or external images.</desc>
<metadata>Visual reference: https://www.playstation.com/en-us/accessories/dualsense-wireless-controller/ . Independently drawn vector artwork.</metadata>
<defs>
<linearGradient id="shell" x1="0" y1="0" x2=".8" y2="1">
<stop stop-color="#3c454b" />
<stop offset="1" stop-color="#20272c" />
</linearGradient>
<radialGradient id="dish" cx="50%" cy="90%" r="100%">
<stop stop-color="#283239" />
<stop offset="1" stop-color="#424f58" />
</radialGradient>
<linearGradient id="white" x1=".15" y1="0" x2=".85" y2="1">
<stop stop-color="#f2f4f7" />
<stop offset="1" stop-color="#bfc8d4" />
</linearGradient>
<g id="stick">
<circle cy="2" r="62" fill="#202a31" stroke="#546169" stroke-width="2" />
<circle cy="1" r="52" fill="#141e25" />
<circle cy="-2" r="47" fill="#536169" stroke="#748189" stroke-width="1.5" />
<circle cy="-2" r="39" fill="url(#dish)" stroke="#1b272f" stroke-width="2.5" />
</g>
<g id="ps-key">
<circle cy="1" r="25" fill="#a3adb9" />
<circle cy="-1" r="23" fill="url(#white)" stroke="#f7f9fb" stroke-width="1.2" />
</g>
<path id="ps-direction" d="M-10-27H10Q21-27 21-16V1Q21 6 16 10L5 20Q0 24-5 20L-16 10Q-21 6-21 1V-16Q-21-27-10-27Z" />
</defs>
<g id="controller">
<g id="shoulders" fill="url(#shell)" stroke="#657179" stroke-width="1.5">
<path d="M113 61V42C142 24 177 23 218 24L219 60Z" />
<path d="M687 61V42C658 24 623 23 582 24L581 60Z" />
</g>
<path d="M133 53C192 34 293 37 400 37C507 37 608 34 667 53C723 107 753 187 770 285C783 365 780 447 756 493C744 516 713 529 677 529C660 528 641 476 614 420C597 385 581 356 553 355C487 357 313 357 247 355C219 356 203 385 186 420C159 476 140 528 123 529C87 529 56 516 44 493C20 447 17 365 30 285C47 187 77 107 133 53Z" fill="url(#shell)" stroke="#172128" stroke-width="2" />
<path id="left-white-shell" d="M133 53C166 43 209 39 247 42L266 148C272 178 263 195 241 222C188 290 144 405 106 510C100 528 87 525 73 514C35 487 25 433 27 363C26 247 60 124 112 66Z" fill="url(#white)" stroke="#aab5c1" stroke-width="1.5" />
<use xlink:href="#left-white-shell" transform="translate(800 0) scale(-1 1)" />
<path d="M120 62C153 49 196 46 242 47M680 62C647 49 604 46 558 47" fill="none" stroke="#fff" stroke-width="2" />
<path d="M249 356C219 357 204 380 184 423C162 470 143 520 130 526M551 356C581 357 596 380 616 423C638 470 657 520 670 526" fill="none" stroke="#5b6872" stroke-width="1.5" />
<g id="touchpad">
<path d="M252 42C294 35 506 35 548 42Q558 45 555 64L537 161Q530 201 497 204H303Q270 201 263 161L245 64Q242 45 252 42Z" fill="#15202b" />
<path d="M251 48L270 157Q276 184 287 190M549 48L530 157Q524 184 513 190" fill="none" stroke="#477df1" stroke-width="5" stroke-linecap="round" />
<path d="M262 42Q400 33 538 42Q551 43 548 60L531 153Q525 190 496 194H304Q275 190 269 153L252 60Q249 43 262 42Z" fill="url(#white)" stroke="#c9d2dc" stroke-width="1.3" />
<path d="M264 46Q400 38 536 46" fill="none" stroke="#fff" stroke-width="1.7" />
</g>
<g id="create-button" transform="translate(231 92) rotate(-13)">
<rect x="-9" y="-17" width="18" height="34" rx="9" fill="url(#white)" stroke="#8d99a5" stroke-width="1.4" />
<path d="M-6-24L-9-29M0-24V-31M6-24L9-29" stroke="#8c99a5" stroke-width="1.2" />
</g>
<g id="options-button" transform="translate(569 92) rotate(13)">
<rect x="-9" y="-17" width="18" height="34" rx="9" fill="url(#white)" stroke="#8d99a5" stroke-width="1.4" />
<path d="M-5-24H5M-5-28H5M-5-32H5" stroke="#8c99a5" stroke-width="1.2" />
</g>
<g id="directional-pad" transform="translate(166 174)">
<g transform="translate(0 -39)">
<use xlink:href="#ps-direction" fill="url(#white)" stroke="#8e99a5" stroke-width="2.7" />
<path d="M-4-10L0-16L4-10Z" fill="#a4afba" />
</g>
<g transform="translate(39 0) rotate(90)">
<use xlink:href="#ps-direction" fill="url(#white)" stroke="#8e99a5" stroke-width="2.7" />
<path d="M-4-10L0-16L4-10Z" fill="#a4afba" />
</g>
<g transform="translate(0 39) rotate(180)">
<use xlink:href="#ps-direction" fill="url(#white)" stroke="#8e99a5" stroke-width="2.7" />
<path d="M-4-10L0-16L4-10Z" fill="#a4afba" />
</g>
<g transform="translate(-39 0) rotate(-90)">
<use xlink:href="#ps-direction" fill="url(#white)" stroke="#8e99a5" stroke-width="2.7" />
<path d="M-4-10L0-16L4-10Z" fill="#a4afba" />
</g>
</g>
<g id="face-buttons" fill="none" stroke="#8d99a8" stroke-width="2.4">
<g transform="translate(634 120)">
<use xlink:href="#ps-key" stroke="none" />
<path d="M0-16L-15 10H15Z" />
</g>
<g transform="translate(580 174)">
<use xlink:href="#ps-key" stroke="none" />
<path d="M-12-13H12V11H-12Z" />
</g>
<g transform="translate(688 174)">
<use xlink:href="#ps-key" stroke="none" />
<circle cy="-1" r="14" />
</g>
<g transform="translate(634 228)">
<use xlink:href="#ps-key" stroke="none" />
<path d="M-11-12L11 10M11-12L-11 10" />
</g>
</g>
<use xlink:href="#stick" transform="translate(282 282) scale(.85)" />
<use xlink:href="#stick" transform="translate(518 282) scale(.85)" />
<g id="ps-button" transform="translate(400 277)" fill="#111b22" stroke="#63717b" stroke-width=".8">
<path d="M-6-22L7-18C18-14 18-3 8 0L3-2V-12L0-13V14L-6 12Z" />
<path d="M-9-3L-22 2C-34 7-21 14-10 11L-10 6C-17 8-23 7-17 5L-9 2ZM4 0C15-3 27 1 24 6C23 8 12 12 3 15V9L16 5C21 3 13 2 5 5Z" />
</g>
<g id="mute-button">
<rect x="-19" y="-5" width="38" height="10" rx="5" transform="translate(400 322)" fill="#131e26" />
<rect x="-17" y="-4" width="34" height="7" rx="3.5" transform="translate(400 322)" fill="#59666f" />
</g>
<g id="microphone" transform="translate(400 342)" stroke="#687984" stroke-width="1.2" fill="none">
<rect x="-2" y="-5" width="4" height="7" rx="2" />
<path d="M-4-1V1Q0 7 4 1V-1M0 5V7M-3 7H3" />
</g>
<g id="speaker" fill="#111b22">
<circle cx="372" cy="218" r="3.3" />
<circle cx="383" cy="218" r="3.3" />
<circle cx="394" cy="218" r="3.3" />
<circle cx="405" cy="218" r="3.3" />
<circle cx="416" cy="218" r="3.3" />
<circle cx="427" cy="218" r="3.3" />
<circle cx="383" cy="229" r="3.3" />
<circle cx="394" cy="229" r="3.3" />
<circle cx="405" cy="229" r="3.3" />
<circle cx="416" cy="229" r="3.3" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -0,0 +1,69 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="560" viewBox="-17 -10 200 560" role="img" aria-labelledby="title description">
<title id="title">Nintendo Switch 2 — left Joy-Con 2</title>
<desc id="description">Front-view left Joy-Con 2 with a graphite shell, light-blue accent ring and attachment edge, analog stick, four directional buttons, minus and capture buttons. Transparent background; four gradients, no filters, textures or external assets.</desc>
<metadata>Visual reference: https://www.nintendo.com/us/store/products/nintendo-switch-2-joy-con-2-l-r-light-blue-light-red-123677/ and https://www.nintendo.com/us/store/products/nintendo-switch-2-joy-con-2-charging-grip-123690/ . Independently drawn vector artwork, without embedded reference images.</metadata>
<defs>
<linearGradient id="shell" x1="0" y1="0" x2="1" y2="1">
<stop stop-color="#3b4348" />
<stop offset="1" stop-color="#20262b" />
</linearGradient>
<linearGradient id="face" x1="0" y1="0" x2=".85" y2="1">
<stop stop-color="#4b555a" />
<stop offset="1" stop-color="#293137" />
</linearGradient>
<linearGradient id="button" x1="0" y1="0" x2=".4" y2="1">
<stop stop-color="#505b62" />
<stop offset="1" stop-color="#303a41" />
</linearGradient>
<radialGradient id="dish" cx="50%" cy="90%" r="100%">
<stop stop-color="#283239" />
<stop offset="1" stop-color="#424f58" />
</radialGradient>
<path id="right-shell" d="M3 5H20C111 5 164 61 164 139V376C164 482 132 537 20 537H3Z" />
<g id="key">
<circle cy="1" r="20.5" fill="#172127" />
<circle cy="-1" r="18.5" fill="url(#button)" stroke="#657178" stroke-width="1.2" />
</g>
<g id="stick">
<circle cy="2" r="46" fill="#172127" />
<circle r="43" fill="#39464d" stroke="currentColor" stroke-width="3" />
<circle cy="-1" r="38.5" fill="#182329" />
<circle cy="-2" r="35.5" fill="#56646c" />
<circle cy="-2" r="29.5" fill="url(#dish)" stroke="#202b32" stroke-width="2" />
<path d="M0-40V-32M-38-2H-31M31-2H38M0 29V36" fill="none" stroke="#1a252c" stroke-width="2" stroke-linecap="round" />
</g>
<g id="capture">
<rect x="-15.5" y="-15.5" width="31" height="31" rx="4" fill="#172127" />
<rect x="-13.5" y="-14" width="27" height="27" rx="2.5" fill="url(#button)" stroke="#626e76" stroke-width="1" />
<circle cy="-1" r="9.5" fill="none" stroke="#1c2830" stroke-width="1.4" />
<path d="M-9 2A9.5 9.5 0 0 0 9 2" fill="none" stroke="#738188" stroke-width=".8" />
</g>
</defs>
<g id="left-joycon" aria-label="Left Joy-Con 2">
<path d="M139 1C61 0 3 48-5 125L1 127C8 59 56 10 139 9Z" fill="url(#shell)" stroke="#667178" stroke-width="1.2" />
<path d="M168 9Q171 9 171 15V524Q171 532 167 533H158V9Z" fill="#77cce8" stroke="#4a9fba" stroke-width="1" />
<use xlink:href="#right-shell" transform="translate(164 0) scale(-1 1)" fill="url(#face)" stroke="#172127" stroke-width="2" />
<path d="M158 9H144C58 9 5 64 4 138" fill="none" stroke="#79848a" stroke-opacity=".7" stroke-width="1.5" />
<path d="M4 390C8 483 39 531 143 533H157" fill="none" stroke="#4c585f" stroke-width="1.2" />
<rect x="119" y="58" width="28" height="9" rx="1.5" fill="#19232a" stroke="#68757d" stroke-width="1" />
<path d="M121 60H145V63H121Z" fill="#48545c" />
<use xlink:href="#stick" transform="translate(84 141)" color="#83d4ee" />
<g transform="translate(84 239)">
<use xlink:href="#key" />
<path d="M0-9L-7 3H7Z" fill="#162128" />
</g>
<g transform="translate(44 278)">
<use xlink:href="#key" />
<path d="M-8-1L5-8V6Z" fill="#162128" />
</g>
<g transform="translate(124 278)">
<use xlink:href="#key" />
<path d="M8-1L-5-8V6Z" fill="#162128" />
</g>
<g transform="translate(84 317)">
<use xlink:href="#key" />
<path d="M0 7L-7-5H7Z" fill="#162128" />
</g>
<use xlink:href="#capture" transform="translate(119 375)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -0,0 +1,76 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="560" viewBox="-19 -10 200 560" role="img" aria-labelledby="title description">
<title id="title">Nintendo Switch 2 — right Joy-Con 2</title>
<desc id="description">Front-view right Joy-Con 2 with a graphite shell, coral accent ring, ABXY buttons, analog stick, plus, home and C buttons. Transparent background; four gradients, no filters, textures or external assets.</desc>
<metadata>Visual reference: https://www.nintendo.com/us/store/products/nintendo-switch-2-joy-con-2-l-r-light-blue-light-red-123677/ and https://www.nintendo.com/us/store/products/nintendo-switch-2-joy-con-2-charging-grip-123690/ . Independently drawn vector artwork, without embedded reference images.</metadata>
<defs>
<linearGradient id="shell" x1="0" y1="0" x2="1" y2="1">
<stop stop-color="#3b4348" />
<stop offset="1" stop-color="#20262b" />
</linearGradient>
<linearGradient id="face" x1="0" y1="0" x2=".85" y2="1">
<stop stop-color="#4b555a" />
<stop offset="1" stop-color="#293137" />
</linearGradient>
<linearGradient id="button" x1="0" y1="0" x2=".4" y2="1">
<stop stop-color="#505b62" />
<stop offset="1" stop-color="#303a41" />
</linearGradient>
<radialGradient id="dish" cx="50%" cy="90%" r="100%">
<stop stop-color="#283239" />
<stop offset="1" stop-color="#424f58" />
</radialGradient>
<path id="right-shell" d="M3 5H20C111 5 164 61 164 139V376C164 482 132 537 20 537H3Z" />
<g id="key">
<circle cy="1" r="20.5" fill="#172127" />
<circle cy="-1" r="18.5" fill="url(#button)" stroke="#657178" stroke-width="1.2" />
</g>
<g id="stick">
<circle cy="2" r="46" fill="#172127" />
<circle r="43" fill="#39464d" stroke="currentColor" stroke-width="3" />
<circle cy="-1" r="38.5" fill="#182329" />
<circle cy="-2" r="35.5" fill="#56646c" />
<circle cy="-2" r="29.5" fill="url(#dish)" stroke="#202b32" stroke-width="2" />
<path d="M0-40V-32M-38-2H-31M31-2H38M0 29V36" fill="none" stroke="#1a252c" stroke-width="2" stroke-linecap="round" />
</g>
<g id="home">
<circle cy="1" r="18" fill="#162027" />
<circle cy="-1" r="16" fill="url(#button)" stroke="#626e76" stroke-width="1.1" />
<path d="M-11-3L0-12L11-3H8V7H-8V-3ZM-3-2V3H3V-2Z" fill="#172127" fill-rule="evenodd" />
</g>
<g id="chat">
<rect x="-15.5" y="-15.5" width="31" height="31" rx="4" fill="#162027" />
<rect x="-13.5" y="-14" width="27" height="27" rx="2.5" fill="url(#button)" stroke="#626e76" stroke-width="1" />
<path d="M6-7C-4-13-12-4-6 5C-3 9 3 9 7 5" fill="none" stroke="#172127" stroke-width="3" />
</g>
</defs>
<g id="right-joycon" aria-label="Right Joy-Con 2">
<path d="M25 1C103 0 161 48 169 125L163 127C156 59 108 10 25 9Z" fill="url(#shell)" stroke="#667178" stroke-width="1.2" />
<path d="M-4 9Q-7 9-7 15V524Q-7 532-3 533H6V9Z" fill="#f38c7d" stroke="#c56960" stroke-width="1" />
<use xlink:href="#right-shell" fill="url(#face)" stroke="#172127" stroke-width="2" />
<path d="M6 9H20C106 9 159 64 160 138" fill="none" stroke="#79848a" stroke-opacity=".7" stroke-width="1.5" />
<path d="M160 390C156 483 125 531 21 533H7" fill="none" stroke="#4c585f" stroke-width="1.2" />
<g transform="translate(31 63)">
<path d="M-4-14H4V-4H14V4H4V14H-4V4H-14V-4H-4Z" fill="#182229" stroke="#68757d" stroke-width="1.1" />
<path d="M-2-12H2V-2H12V2H2V12H-2V2H-12V-2H-2Z" fill="url(#button)" />
</g>
<g transform="translate(80 101)">
<use xlink:href="#key" />
<path d="M-6-9L6 7M6-9L-6 7" fill="none" stroke="#bcc5c9" stroke-width="1.7" />
</g>
<g transform="translate(40 140)">
<use xlink:href="#key" />
<path d="M-6-9L0-1L6-9M0-1V7" fill="none" stroke="#bcc5c9" stroke-width="1.7" />
</g>
<g transform="translate(120 140)">
<use xlink:href="#key" />
<path d="M-7 7L0-10L7 7M-4.5 2H4.5" fill="none" stroke="#bcc5c9" stroke-width="1.7" stroke-linejoin="round" />
</g>
<g transform="translate(80 179)">
<use xlink:href="#key" />
<path d="M-5-9V7H1C9 7 9-1 1-1H-5M-5-9H1C8-9 8-1 1-1" fill="none" stroke="#bcc5c9" stroke-width="1.7" stroke-linejoin="round" />
</g>
<use xlink:href="#stick" transform="translate(80 278)" color="#f58d80" />
<use xlink:href="#home" transform="translate(45 375)" />
<use xlink:href="#chat" transform="translate(45 431)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -0,0 +1,143 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="688" height="560" viewBox="-8 -10 688 560" role="img" aria-labelledby="title description">
<title id="title">Nintendo Switch 2 — connected Joy-Con 2 pair</title>
<desc id="description">Front-view left and right Joy-Con 2 attached to a central controller grip. Graphite shells, light-blue and coral stick rings, directional and ABXY buttons, capture, home and C buttons. Transparent background; four gradients, no filters, textures or external assets.</desc>
<metadata>Visual reference: https://www.nintendo.com/us/store/products/nintendo-switch-2-joy-con-2-l-r-light-blue-light-red-123677/ and https://www.nintendo.com/us/store/products/nintendo-switch-2-joy-con-2-charging-grip-123690/ . Independently drawn vector artwork, without embedded reference images.</metadata>
<defs>
<linearGradient id="shell" x1="0" y1="0" x2="1" y2="1">
<stop stop-color="#3b4348" />
<stop offset="1" stop-color="#20262b" />
</linearGradient>
<linearGradient id="face" x1="0" y1="0" x2=".85" y2="1">
<stop stop-color="#4b555a" />
<stop offset="1" stop-color="#293137" />
</linearGradient>
<linearGradient id="button" x1="0" y1="0" x2=".4" y2="1">
<stop stop-color="#505b62" />
<stop offset="1" stop-color="#303a41" />
</linearGradient>
<radialGradient id="dish" cx="50%" cy="90%" r="100%">
<stop stop-color="#283239" />
<stop offset="1" stop-color="#424f58" />
</radialGradient>
<path id="right-shell" d="M3 5H20C111 5 164 61 164 139V376C164 482 132 537 20 537H3Z" />
<g id="key">
<circle cy="1" r="20.5" fill="#172127" />
<circle cy="-1" r="18.5" fill="url(#button)" stroke="#657178" stroke-width="1.2" />
</g>
<g id="stick">
<circle cy="2" r="46" fill="#172127" />
<circle r="43" fill="#39464d" stroke="currentColor" stroke-width="3" />
<circle cy="-1" r="38.5" fill="#182329" />
<circle cy="-2" r="35.5" fill="#56646c" />
<circle cy="-2" r="29.5" fill="url(#dish)" stroke="#202b32" stroke-width="2" />
<path d="M0-40V-32M-38-2H-31M31-2H38M0 29V36" fill="none" stroke="#1a252c" stroke-width="2" stroke-linecap="round" />
</g>
<g id="home">
<circle cy="1" r="18" fill="#162027" />
<circle cy="-1" r="16" fill="url(#button)" stroke="#626e76" stroke-width="1.1" />
<path d="M-11-3L0-12L11-3H8V7H-8V-3ZM-3-2V3H3V-2Z" fill="#172127" fill-rule="evenodd" />
</g>
<g id="chat">
<rect x="-15.5" y="-15.5" width="31" height="31" rx="4" fill="#162027" />
<rect x="-13.5" y="-14" width="27" height="27" rx="2.5" fill="url(#button)" stroke="#626e76" stroke-width="1" />
<path d="M6-7C-4-13-12-4-6 5C-3 9 3 9 7 5" fill="none" stroke="#172127" stroke-width="3" />
</g>
<g id="capture">
<rect x="-15.5" y="-15.5" width="31" height="31" rx="4" fill="#172127" />
<rect x="-13.5" y="-14" width="27" height="27" rx="2.5" fill="url(#button)" stroke="#626e76" stroke-width="1" />
<circle cy="-1" r="9.5" fill="none" stroke="#1c2830" stroke-width="1.4" />
<path d="M-9 2A9.5 9.5 0 0 0 9 2" fill="none" stroke="#738188" stroke-width=".8" />
</g>
</defs>
<g id="grip" aria-label="Connecting Joy-Con grip">
<path id="grip-wing" d="M82 164C41 188 27 287 12 398C0 470-1 517 22 536C42 553 69 535 94 507L125 339Z" fill="url(#shell)" stroke="#1b252b" stroke-width="2" />
<use xlink:href="#grip-wing" transform="translate(672 0) scale(-1 1)" />
<path d="M71 185C43 242 31 331 21 404C12 470 10 510 30 525" fill="none" stroke="#66747c" stroke-width="3" />
<path d="M601 185C629 242 641 331 651 404C660 470 662 510 642 525" fill="none" stroke="#66747c" stroke-width="3" />
<path d="M219 5H453V538H219Z" fill="#141f26" />
<path d="M231 5H441Q444 5 444 9V537H228V9Q228 5 231 5Z" fill="url(#shell)" stroke="#182228" stroke-width="1.5" />
<path d="M231 9H441" stroke="#7b878d" stroke-width="2" />
<path d="M231 534H441" stroke="#4d5961" stroke-width="1.5" />
</g>
<g transform="translate(64 0)">
<g id="left-joycon" aria-label="Left Joy-Con 2">
<path d="M139 1C61 0 3 48-5 125L1 127C8 59 56 10 139 9Z" fill="url(#shell)" stroke="#667178" stroke-width="1.2" />
<path d="M161 9H165V532H161Z" fill="#77cce8" />
<use xlink:href="#right-shell" transform="translate(164 0) scale(-1 1)" fill="url(#face)" stroke="#172127" stroke-width="2" />
<path d="M158 9H144C58 9 5 64 4 138" fill="none" stroke="#79848a" stroke-opacity=".7" stroke-width="1.5" />
<path d="M4 390C8 483 39 531 143 533H157" fill="none" stroke="#4c585f" stroke-width="1.2" />
<rect x="119" y="58" width="28" height="9" rx="1.5" fill="#19232a" stroke="#68757d" stroke-width="1" />
<path d="M121 60H145V63H121Z" fill="#48545c" />
<use xlink:href="#stick" transform="translate(84 141)" color="#83d4ee" />
<g transform="translate(84 239)">
<use xlink:href="#key" />
<path d="M0-9L-7 3H7Z" fill="#162128" />
</g>
<g transform="translate(44 278)">
<use xlink:href="#key" />
<path d="M-8-1L5-8V6Z" fill="#162128" />
</g>
<g transform="translate(124 278)">
<use xlink:href="#key" />
<path d="M8-1L-5-8V6Z" fill="#162128" />
</g>
<g transform="translate(84 317)">
<use xlink:href="#key" />
<path d="M0 7L-7-5H7Z" fill="#162128" />
</g>
<use xlink:href="#capture" transform="translate(119 375)" />
</g>
</g>
<g transform="translate(444 0)">
<g id="right-joycon" aria-label="Right Joy-Con 2">
<path d="M25 1C103 0 161 48 169 125L163 127C156 59 108 10 25 9Z" fill="url(#shell)" stroke="#667178" stroke-width="1.2" />
<path d="M1 9H5V533H1Z" fill="#f38c7d" stroke="#c56960" stroke-width="1" />
<use xlink:href="#right-shell" fill="url(#face)" stroke="#172127" stroke-width="2" />
<path d="M6 9H20C106 9 159 64 160 138" fill="none" stroke="#79848a" stroke-opacity=".7" stroke-width="1.5" />
<path d="M160 390C156 483 125 531 21 533H7" fill="none" stroke="#4c585f" stroke-width="1.2" />
<g transform="translate(31 63)">
<path d="M-4-14H4V-4H14V4H4V14H-4V4H-14V-4H-4Z" fill="#182229" stroke="#68757d" stroke-width="1.1" />
<path d="M-2-12H2V-2H12V2H2V12H-2V2H-12V-2H-2Z" fill="url(#button)" />
</g>
<g transform="translate(80 101)">
<use xlink:href="#key" />
<path d="M-6-9L6 7M6-9L-6 7" fill="none" stroke="#bcc5c9" stroke-width="1.7" />
</g>
<g transform="translate(40 140)">
<use xlink:href="#key" />
<path d="M-6-9L0-1L6-9M0-1V7" fill="none" stroke="#bcc5c9" stroke-width="1.7" />
</g>
<g transform="translate(120 140)">
<use xlink:href="#key" />
<path d="M-7 7L0-10L7 7M-4.5 2H4.5" fill="none" stroke="#bcc5c9" stroke-width="1.7" stroke-linejoin="round" />
</g>
<g transform="translate(80 179)">
<use xlink:href="#key" />
<path d="M-5-9V7H1C9 7 9-1 1-1H-5M-5-9H1C8-9 8-1 1-1" fill="none" stroke="#bcc5c9" stroke-width="1.7" stroke-linejoin="round" />
</g>
<use xlink:href="#stick" transform="translate(80 278)" color="#f58d80" />
<use xlink:href="#home" transform="translate(45 375)" />
<use xlink:href="#chat" transform="translate(45 431)" />
</g>
</g>
<g id="console-mark" transform="translate(266 208) scale(.87)" fill="#151e23">
<path d="M36 0H22C9 0 0 10 0 23V59C0 72 9 82 22 82H36ZM29 7V75H22C13 75 7 69 7 59V23C7 13 13 7 22 7Z" fill-rule="evenodd" />
<circle cx="21" cy="25" r="7.8" />
<path d="M46 0H61C74 0 82 10 82 23V59C82 72 74 82 61 82H46ZM64 37A8 8 0 1 0 64 53A8 8 0 1 0 64 37Z" fill-rule="evenodd" />
<path d="M94 17 C103 4 113-3 127-3 C147-3 158 8 158 25 C158 42 146 52 119 66 H159 V82 H94 V68 C136 35 139 32 138 24 C138 17 133 14 127 14 C119 14 112 19 106 27Z" />
<g font-family="Arial, Helvetica, sans-serif" text-anchor="middle" font-weight="700">
<path aria-label="NINTENDO" d="M26.371 111 21.274 101.994Q21.424 103.305 21.424 104.102V111H19.249V99.304H22.046L27.218 108.385Q27.068 107.132 27.068 106.103V99.304H29.243V111ZM36.526 111V99.304H38.974V111ZM53.371 111 48.274 101.994Q48.424 103.305 48.424 104.102V111H46.249V99.304H49.046L54.218 108.385Q54.068 107.132 54.068 106.103V99.304H56.243V111ZM68.805 101.197V111H66.356V101.197H62.579V99.304H72.59V101.197ZM78.91 111V99.304H88.107V101.197H81.359V104.135H87.601V106.028H81.359V109.107H88.448V111ZM102.371 111 97.274 101.994Q97.424 103.305 97.424 104.102V111H95.249V99.304H98.046L103.218 108.385Q103.068 107.132 103.068 106.103V99.304H105.243V111ZM122.951 105.065Q122.951 106.875 122.242 108.223Q121.532 109.572 120.233 110.286Q118.934 111 117.257 111H112.526V99.304H116.759Q119.714 99.304 121.333 100.794Q122.951 102.284 122.951 105.065ZM120.486 105.065Q120.486 103.181 119.507 102.189Q118.527 101.197 116.709 101.197H114.974V109.107H117.05Q118.627 109.107 119.556 108.02Q120.486 106.933 120.486 105.065ZM141.175 105.098Q141.175 106.924 140.452 108.311Q139.73 109.697 138.385 110.431Q137.041 111.166 135.248 111.166Q132.492 111.166 130.927 109.543Q129.363 107.92 129.363 105.098Q129.363 102.284 130.923 100.707Q132.484 99.13 135.264 99.13Q138.045 99.13 139.61 100.724Q141.175 102.317 141.175 105.098ZM138.676 105.098Q138.676 103.206 137.78 102.131Q136.883 101.056 135.264 101.056Q133.621 101.056 132.724 102.122Q131.828 103.189 131.828 105.098Q131.828 107.024 132.745 108.132Q133.662 109.24 135.248 109.24Q136.891 109.24 137.784 108.161Q138.676 107.082 138.676 105.098Z" />
<path aria-label="SWITCH" d="M33.054 135.26Q33.054 138.696 30.506 140.514Q27.958 142.332 23.027 142.332Q18.528 142.332 15.971 140.738Q13.415 139.145 12.684 135.907L17.416 135.127Q17.897 136.986 19.292 137.825Q20.686 138.663 23.16 138.663Q28.29 138.663 28.29 135.542Q28.29 134.546 27.7 133.898Q27.111 133.251 26.04 132.819Q24.969 132.388 21.931 131.773Q19.308 131.159 18.279 130.786Q17.25 130.412 16.419 129.906Q15.589 129.399 15.008 128.686Q14.427 127.972 14.104 127.009Q13.78 126.046 13.78 124.801Q13.78 121.63 16.162 119.945Q18.544 118.26 23.093 118.26Q27.443 118.26 29.626 119.621Q31.809 120.982 32.44 124.12L27.692 124.768Q27.327 123.257 26.206 122.493Q25.085 121.729 22.994 121.729Q18.544 121.729 18.544 124.519Q18.544 125.432 19.018 126.013Q19.491 126.594 20.42 127Q21.35 127.407 24.189 128.021Q27.559 128.735 29.012 129.341Q30.464 129.947 31.311 130.752Q32.158 131.558 32.606 132.678Q33.054 133.799 33.054 135.26ZM60.897 142H55.086L51.916 128.47Q51.334 126.079 50.936 123.473Q50.538 125.647 50.289 126.785Q50.04 127.922 46.752 142H40.942L34.916 118.608H39.879L43.266 133.716L44.03 137.368Q44.495 135.061 44.935 132.96Q45.375 130.86 48.247 118.608H53.725L56.68 131.06Q57.029 132.454 57.859 137.368L58.274 135.442L59.154 131.624L61.976 118.608H66.94ZM69.748 142V118.608H74.645V142ZM90.252 122.394V142H85.355V122.394H77.801V118.608H97.823V122.394ZM111.886 138.48Q116.319 138.48 118.045 134.031L122.312 135.642Q120.934 139.028 118.27 140.68Q115.605 142.332 111.886 142.332Q106.242 142.332 103.162 139.136Q100.083 135.94 100.083 130.196Q100.083 124.436 103.054 121.348Q106.026 118.26 111.67 118.26Q115.788 118.26 118.377 119.912Q120.967 121.563 122.013 124.768L117.697 125.946Q117.149 124.187 115.547 123.149Q113.945 122.111 111.77 122.111Q108.45 122.111 106.731 124.17Q105.013 126.229 105.013 130.196Q105.013 134.23 106.781 136.355Q108.549 138.48 111.886 138.48ZM141.107 142V131.973H130.914V142H126.016V118.608H130.914V127.922H141.107V118.608H146.004V142Z" />
</g>
</g>
<g id="player-indicators" fill="#536068">
<rect x="237" y="244" width="6" height="7" rx="1.2" />
<rect x="237" y="263" width="6" height="7" rx="1.2" />
<rect x="237" y="282" width="6" height="7" rx="1.2" />
<rect x="237" y="301" width="6" height="7" rx="1.2" />
<rect x="429" y="244" width="6" height="7" rx="1.2" />
<rect x="429" y="263" width="6" height="7" rx="1.2" />
<rect x="429" y="282" width="6" height="7" rx="1.2" />
<rect x="429" y="301" width="6" height="7" rx="1.2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,132 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1232" height="884" viewBox="-16 160 1232 884" role="img" aria-labelledby="title description">
<title id="title">Nintendo Switch 2 Pro Controller — lightweight transparent illustration</title>
<desc id="description">A simplified graphite and ivory Nintendo Switch 2 Pro Controller on a transparent background. Five simple gradients and solid vector shapes; no filters, textures, masks, shadows, fonts, or external assets.</desc>
<metadata>Reference: https://www.nintendo.com/us/store/products/nintendo-switch-2-pro-controller-123674/ . Original vector illustration; no embedded photographs or external assets.</metadata>
<defs>
<linearGradient id="shell" x1=".1" y1="0" x2=".85" y2="1" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#3b4348" />
<stop offset="1" stop-color="#20262b" />
</linearGradient>
<radialGradient id="face" cx="43%" cy="-18%" r="114%" gradientTransform="translate(0 -.06) scale(1 1.06)">
<stop offset="0" stop-color="#4b555a" />
<stop offset="1" stop-color="#293137" />
</radialGradient>
<linearGradient id="ivory" x1="0" y1="0" x2=".15" y2="1">
<stop offset="0" stop-color="#e6e8e4" />
<stop offset="1" stop-color="#b5bec2" />
</linearGradient>
<linearGradient id="buttonTop" x1=".3" y1="0" x2=".6" y2="1">
<stop offset="0" stop-color="#505b62" />
<stop offset="1" stop-color="#303a41" />
</linearGradient>
<radialGradient id="stickDish" cx="49%" cy="95%" r="107%">
<stop offset="0" stop-color="#283239" />
<stop offset="1" stop-color="#424f58" />
</radialGradient>
<path id="bodyShape" d="M132 278 C153 234 219 211 319 205 C470 188 730 188 881 205 C981 211 1047 234 1068 278 C1102 312 1118 395 1133 477 C1158 613 1180 743 1195 846 C1209 940 1180 1004 1115 1023 C1061 1041 1022 1006 992 957 C958 902 933 838 907 800 C898 786 888 778 862 778 L338 778 C312 778 302 786 293 800 C267 838 242 902 208 957 C178 1006 139 1041 85 1023 C20 1004 -9 940 5 846 C20 743 42 613 67 477 C82 395 98 312 132 278 Z" />
<path id="faceShape" d="M133 282 C211 216 336 202 600 202 C864 202 989 216 1067 282 C1080 327 1089 386 1082 451 C1077 536 1036 618 977 696 C950 733 914 748 858 749 L342 749 C286 748 250 733 223 696 C164 618 123 536 118 451 C111 386 120 327 133 282 Z" />
<g id="actionButton">
<circle cy="2" r="49.5" fill="#14191c" stroke="#51585b" stroke-opacity=".38" stroke-width="1.5" />
<circle cy="1" r="46.5" fill="#1b242a" />
<circle cy="-2" r="44.5" fill="url(#buttonTop)" stroke="#687278" stroke-width="1.8" />
<path d="M-36-27 A44 44 0 0 1 29-35" fill="none" stroke="#b1b8b9" stroke-width="1.15" opacity=".22" stroke-linecap="round" />
</g>
<g id="smallButton">
<circle cy="1" r="31" fill="#0c1114" stroke="#62696a" stroke-opacity=".3" stroke-width="1.5" />
<circle cy="1" r="28" fill="#1b242a" />
<circle cy="-2" r="26.5" fill="url(#buttonTop)" stroke="#687278" stroke-width="1.7" />
</g>
<g id="analogStick">
<circle r="106" fill="#3f484d" />
<circle r="96" cy="2" fill="#222a2f" />
<circle r="78" cy="1" fill="#131c22" />
<circle r="72" cy="-5" fill="#68747b" stroke="#8d989c" stroke-width="2" />
<circle r="60" cy="-5" fill="url(#stickDish)" stroke="#172127" stroke-width="3" />
</g>
</defs>
<g id="controller">
<g id="shoulder-buttons">
<path d="M127 289 C139 226 207 185 342 177 C366 174 377 177 398 187 L443 209 L244 270 Z" fill="url(#ivory)" stroke="#a3aaab" stroke-width="1.6" />
<path d="M1073 289 C1061 226 993 185 858 177 C834 174 823 177 802 187 L757 209 L956 270 Z" fill="url(#ivory)" stroke="#a3aaab" stroke-width="1.6" />
<path d="M141 258 C170 210 240 186 342 180 C365 177 379 181 395 189" fill="none" stroke="#eceeeb" stroke-width="2" />
<path d="M1059 258 C1030 210 960 186 858 180 C835 177 821 181 805 189" fill="none" stroke="#eceeeb" stroke-width="2" />
<path d="M416 201 Q600 185 784 201 L806 222 H394Z" fill="#a5acae" stroke="#cbd0d0" stroke-width="1.5" />
<path d="M448 198 Q600 190 752 198" stroke="#eff0ed" stroke-opacity=".65" stroke-width="2" fill="none" />
</g>
<use xlink:href="#bodyShape" fill="url(#shell)" stroke="#171c20" stroke-width="2" />
<use xlink:href="#faceShape" fill="url(#face)" stroke="#101619" stroke-width="2.4" />
<path d="M134 279 C222 218 365 202 600 202 C835 202 978 218 1066 279" stroke="#687275" stroke-width="2.2" fill="none" />
<path d="M124 322 C105 459 138 574 225 694 C251 730 287 746 342 747 H858 C913 746 949 730 975 694" fill="none" stroke="#778083" stroke-opacity=".1" stroke-width="1.2" />
<path d="M339 779 H861" stroke="#737e81" stroke-opacity=".23" stroke-width="1.6" />
<g id="console-mark" transform="translate(520 242)" fill="#151e23">
<path d="M36 0H22C9 0 0 10 0 23V59C0 72 9 82 22 82H36ZM29 7V75H22C13 75 7 69 7 59V23C7 13 13 7 22 7Z" fill-rule="evenodd" />
<circle cx="21" cy="25" r="7.8" />
<path d="M46 0H61C74 0 82 10 82 23V59C82 72 74 82 61 82H46ZM64 37A8 8 0 1 0 64 53A8 8 0 1 0 64 37Z" fill-rule="evenodd" />
<path d="M94 17 C103 4 113-3 127-3 C147-3 158 8 158 25 C158 42 146 52 119 66 H159 V82 H94 V68 C136 35 139 32 138 24 C138 17 133 14 127 14 C119 14 112 19 106 27Z" />
<g>
<path aria-label="NINTENDO" d="M26.371 111 21.274 101.994Q21.424 103.305 21.424 104.102V111H19.249V99.304H22.046L27.218 108.385Q27.068 107.132 27.068 106.103V99.304H29.243V111ZM36.526 111V99.304H38.974V111ZM53.371 111 48.274 101.994Q48.424 103.305 48.424 104.102V111H46.249V99.304H49.046L54.218 108.385Q54.068 107.132 54.068 106.103V99.304H56.243V111ZM68.805 101.197V111H66.356V101.197H62.579V99.304H72.59V101.197ZM78.91 111V99.304H88.107V101.197H81.359V104.135H87.601V106.028H81.359V109.107H88.448V111ZM102.371 111 97.274 101.994Q97.424 103.305 97.424 104.102V111H95.249V99.304H98.046L103.218 108.385Q103.068 107.132 103.068 106.103V99.304H105.243V111ZM122.951 105.065Q122.951 106.875 122.242 108.223Q121.532 109.572 120.233 110.286Q118.934 111 117.257 111H112.526V99.304H116.759Q119.714 99.304 121.333 100.794Q122.951 102.284 122.951 105.065ZM120.486 105.065Q120.486 103.181 119.507 102.189Q118.527 101.197 116.709 101.197H114.974V109.107H117.05Q118.627 109.107 119.556 108.02Q120.486 106.933 120.486 105.065ZM141.175 105.098Q141.175 106.924 140.452 108.311Q139.73 109.697 138.385 110.431Q137.041 111.166 135.248 111.166Q132.492 111.166 130.927 109.543Q129.363 107.92 129.363 105.098Q129.363 102.284 130.923 100.707Q132.484 99.13 135.264 99.13Q138.045 99.13 139.61 100.724Q141.175 102.317 141.175 105.098ZM138.676 105.098Q138.676 103.206 137.78 102.131Q136.883 101.056 135.264 101.056Q133.621 101.056 132.724 102.122Q131.828 103.189 131.828 105.098Q131.828 107.024 132.745 108.132Q133.662 109.24 135.248 109.24Q136.891 109.24 137.784 108.161Q138.676 107.082 138.676 105.098Z" />
<path aria-label="SWITCH" d="M33.054 135.26Q33.054 138.696 30.506 140.514Q27.958 142.332 23.027 142.332Q18.528 142.332 15.971 140.738Q13.415 139.145 12.684 135.907L17.416 135.127Q17.897 136.986 19.292 137.825Q20.686 138.663 23.16 138.663Q28.29 138.663 28.29 135.542Q28.29 134.546 27.7 133.898Q27.111 133.251 26.04 132.819Q24.969 132.388 21.931 131.773Q19.308 131.159 18.279 130.786Q17.25 130.412 16.419 129.906Q15.589 129.399 15.008 128.686Q14.427 127.972 14.104 127.009Q13.78 126.046 13.78 124.801Q13.78 121.63 16.162 119.945Q18.544 118.26 23.093 118.26Q27.443 118.26 29.626 119.621Q31.809 120.982 32.44 124.12L27.692 124.768Q27.327 123.257 26.206 122.493Q25.085 121.729 22.994 121.729Q18.544 121.729 18.544 124.519Q18.544 125.432 19.018 126.013Q19.491 126.594 20.42 127Q21.35 127.407 24.189 128.021Q27.559 128.735 29.012 129.341Q30.464 129.947 31.311 130.752Q32.158 131.558 32.606 132.678Q33.054 133.799 33.054 135.26ZM60.897 142H55.086L51.916 128.47Q51.334 126.079 50.936 123.473Q50.538 125.647 50.289 126.785Q50.04 127.922 46.752 142H40.942L34.916 118.608H39.879L43.266 133.716L44.03 137.368Q44.495 135.061 44.935 132.96Q45.375 130.86 48.247 118.608H53.725L56.68 131.06Q57.029 132.454 57.859 137.368L58.274 135.442L59.154 131.624L61.976 118.608H66.94ZM69.748 142V118.608H74.645V142ZM90.252 122.394V142H85.355V122.394H77.801V118.608H97.823V122.394ZM111.886 138.48Q116.319 138.48 118.045 134.031L122.312 135.642Q120.934 139.028 118.27 140.68Q115.605 142.332 111.886 142.332Q106.242 142.332 103.162 139.136Q100.083 135.94 100.083 130.196Q100.083 124.436 103.054 121.348Q106.026 118.26 111.67 118.26Q115.788 118.26 118.377 119.912Q120.967 121.563 122.013 124.768L117.697 125.946Q117.149 124.187 115.547 123.149Q113.945 122.111 111.77 122.111Q108.45 122.111 106.731 124.17Q105.013 126.229 105.013 130.196Q105.013 134.23 106.781 136.355Q108.549 138.48 111.886 138.48ZM141.107 142V131.973H130.914V142H126.016V118.608H130.914V127.922H141.107V118.608H146.004V142Z" />
</g>
</g>
<g id="minus-button" transform="translate(454 341)">
<use xlink:href="#smallButton" />
<path d="M-13-4H13V2H-13Z" fill="#141b20" />
<path d="M-13 3H13" stroke="#778186" stroke-opacity=".45" />
</g>
<g id="plus-button" transform="translate(743 341)">
<use xlink:href="#smallButton" />
<path d="M-3-16H3V-5H14V1H3V12H-3V1H-14V-5H-3Z" fill="#11191e" />
<path d="M-14 2H-4V13H3 M4 2H14" stroke="#7a8589" stroke-opacity=".35" fill="none" />
</g>
<g id="left-stick" transform="translate(272 436)">
<use xlink:href="#analogStick" />
</g>
<g id="right-stick" transform="translate(746 607)">
<use xlink:href="#analogStick" />
</g>
<g id="action-buttons">
<g transform="translate(909 351)">
<use xlink:href="#actionButton" />
<path d="M-11-17L11 13M11-17L-11 13" fill="none" stroke="#bac0c1" stroke-width="3" stroke-linecap="square" />
</g>
<g transform="translate(813 434)">
<use xlink:href="#actionButton" />
<path d="M-11-17L0 0L11-17M0 0V13" fill="none" stroke="#bac0c1" stroke-width="3" stroke-linecap="square" stroke-linejoin="round" />
</g>
<g transform="translate(1005 434)">
<use xlink:href="#actionButton" />
<path d="M-13 13L-1-17H1L13 13M-8 3H8" fill="none" stroke="#bac0c1" stroke-width="3" stroke-linejoin="round" stroke-linecap="square" />
</g>
<g transform="translate(909 518)">
<use xlink:href="#actionButton" />
<path d="M-9-17V13H2C17 13 17-3 3-3H-9M-9-17H1C14-17 15-3 3-3" fill="none" stroke="#bac0c1" stroke-width="3" stroke-linejoin="round" />
</g>
</g>
<g id="capture-button" transform="translate(526 434)">
<rect x="-28" y="-28" width="56" height="56" rx="9" fill="#11171b" stroke="#667073" stroke-opacity=".24" stroke-width="1.3" />
<rect x="-24.5" y="-25" width="49" height="49" rx="6" fill="url(#buttonTop)" stroke="#687278" stroke-width="1.8" />
<circle cy="-1" r="17" fill="none" stroke="#101b21" stroke-width="2" />
<path d="M-16 5A17 17 0 0 0 16 5" fill="none" stroke="#7a858a" stroke-opacity=".6" stroke-width="1.5" />
</g>
<g id="home-button" transform="translate(672 435)">
<use xlink:href="#smallButton" />
<path d="M-18-5L0-20L18-5H12V11H-12V-5ZM-4-3V5H4V-3Z" fill="#11191e" fill-rule="evenodd" />
<path d="M-12 12H12 M-4-2H4" fill="none" stroke="#929a9d" stroke-opacity=".35" />
</g>
<g id="directional-pad" transform="translate(424 602)">
<path d="M-22-99H23Q36-99 36-86V-36H85Q99-36 99-22V23Q99 37 85 37H36V86Q36 99 23 99H-23Q-36 99-36 86V37H-85Q-99 37-99 23V-22Q-99-36-85-36H-36V-86Q-36-99-22-99Z" fill="#11171b" stroke="#50595c" stroke-opacity=".3" stroke-width="1.5" />
<path d="M-21-91H22Q29-91 29-83V-30H83Q91-30 91-22V22Q91 30 83 30H29V83Q29 91 21 91H-21Q-29 91-29 83V30H-83Q-91 30-91 22V-22Q-91-30-83-30H-29V-83Q-29-91-21-91Z" fill="#141d23" />
<path d="M-20-88H20Q27-88 27-81V-30Q27-27 31-27H81Q88-27 88-20V19Q88 26 81 26H31Q27 26 27 30V80Q27 87 20 87H-20Q-27 87-27 80V30Q-27 26-31 26H-81Q-88 26-88 19V-20Q-88-27-81-27H-31Q-27-27-27-30V-81Q-27-88-20-88Z" fill="#657178" />
<path d="M-18-81H18Q21-81 21-77V-31Q21-20 32-20H77Q81-20 81-17V16Q81 19 77 19H32Q21 19 21 30V76Q21 80 18 80H-18Q-21 80-21 76V30Q-21 19-32 19H-77Q-81 19-81 16V-17Q-81-20-77-20H-32Q-21-20-21-31V-77Q-21-81-18-81Z" fill="url(#buttonTop)" />
<path d="M-21-84H19 M-84-21H-33Q-23-21-23-32V-78 M31-22H80" stroke="#adb5b7" stroke-opacity=".4" stroke-width="1.6" fill="none" stroke-linecap="round" />
<path d="M-6-53L0-65L7-53Z M-6 56L0 68L7 56Z M-54-6L-66 1L-54 8Z M55-6L67 1L55 8Z" fill="#b0b8ba" opacity=".78" />
<circle cy="-1" r="15" fill="#202a31" opacity=".13" />
</g>
<g id="chat-button" transform="translate(599 694)">
<rect x="-27" y="-27" width="54" height="54" rx="8" fill="#0c1216" stroke="#717b7e" stroke-opacity=".3" stroke-width="1.5" />
<rect x="-23.5" y="-24" width="47" height="47" rx="5" fill="url(#buttonTop)" stroke="#687278" stroke-width="1.4" />
<path d="M10-10C-7-20-19-6-10 7C-5 14 4 14 11 8" fill="none" stroke="#10181d" stroke-width="5.5" stroke-linecap="square" />
<path d="M-10 9C-5 15 5 16 12 10" fill="none" stroke="#8b9599" stroke-opacity=".35" stroke-width="1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,113 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" height="570" viewBox="0 0 800 570" role="img" aria-labelledby="title description">
<title id="title">Nintendo Switch Pro Controller — original model</title>
<desc id="description">Front-view original Nintendo Switch Pro Controller, with a smoky charcoal face, separate dark grip panels, black shoulder buttons, offset analog sticks, cross directional pad, Nintendo ABXY layout, capture and home buttons, and lower player indicators. Transparent background. Lightweight vector shapes and simple gradients; no filters, textures, fonts or external images.</desc>
<metadata>Visual reference: https://www.nintendo.com/us/store/products/pro-controller/ . Independently drawn vector artwork.</metadata>
<defs>
<linearGradient id="shell" x1="0" y1="0" x2=".8" y2="1">
<stop stop-color="#3c454b" />
<stop offset="1" stop-color="#20272c" />
</linearGradient>
<linearGradient id="face" x1=".2" y1="0" x2=".75" y2="1">
<stop stop-color="#4b555a" />
<stop offset="1" stop-color="#293137" />
</linearGradient>
<linearGradient id="button" x1="0" y1="0" x2=".3" y2="1">
<stop stop-color="#505b62" />
<stop offset="1" stop-color="#303a41" />
</linearGradient>
<radialGradient id="dish" cx="50%" cy="90%" r="100%">
<stop stop-color="#283239" />
<stop offset="1" stop-color="#424f58" />
</radialGradient>
<g id="stick">
<circle cy="2" r="62" fill="#202a31" stroke="#546169" stroke-width="2" />
<circle cy="1" r="52" fill="#141e25" />
<circle cy="-2" r="47" fill="#536169" stroke="#748189" stroke-width="1.5" />
<circle cy="-2" r="39" fill="url(#dish)" stroke="#1b272f" stroke-width="2.5" />
</g>
<g id="key">
<circle cy="2" r="29" fill="#172128" />
<circle cy="-1" r="26.5" fill="url(#button)" stroke="#657179" stroke-width="1.3" />
</g>
<g id="small-key">
<circle cy="1" r="19" fill="#172128" />
<circle cy="-1" r="17" fill="url(#button)" stroke="#657179" stroke-width="1.1" />
</g>
<g id="capture">
<rect x="-17" y="-17" width="34" height="34" rx="5" fill="#172128" />
<rect x="-14.5" y="-15" width="29" height="29" rx="3" fill="url(#button)" stroke="#657179" stroke-width="1.2" />
<circle cy="-1" r="10.5" fill="none" stroke="#202c33" stroke-width="1.5" />
</g>
<g id="home">
<use xlink:href="#small-key" />
<path d="M-11-3L0-13L11-3H8V8H-8V-3ZM-3-2V4H3V-2Z" fill="#18232b" fill-rule="evenodd" />
</g>
</defs>
<g id="controller">
<g id="shoulder-buttons" fill="url(#shell)" stroke="#647079" stroke-width="1.4">
<path d="M105 94C114 49 155 23 231 22L275 46L198 98Z" />
<path d="M695 94C686 49 645 23 569 22L525 46L602 98Z" />
</g>
<path d="M153 57C218 34 300 40 400 39C500 40 582 34 647 57C696 75 720 128 738 220C754 301 776 414 780 458C785 512 763 543 723 552C681 560 658 529 631 480L597 421Q586 405 566 405H234Q214 405 203 421L169 480C142 529 119 560 77 552C37 543 15 512 20 458C24 414 46 301 62 220C80 128 104 75 153 57Z" fill="url(#shell)" stroke="#182229" stroke-width="2" />
<path d="M153 57C218 34 300 40 400 39C500 40 582 34 647 57C696 75 716 122 733 207C684 223 646 257 616 303C585 351 574 385 566 405H234C226 385 215 351 184 303C154 257 116 223 67 207C84 122 104 75 153 57Z" fill="url(#face)" stroke="#182229" stroke-width="1.8" />
<path d="M107 112C124 81 139 69 157 63C223 41 302 45 400 44C498 45 577 41 643 63C661 69 676 81 693 112" fill="none" stroke="#87939a" stroke-width="1.6" />
<path d="M69 212C120 232 160 270 188 313C213 351 224 381 230 402M731 212C680 232 640 270 612 313C587 351 576 381 570 402" fill="none" stroke="#637179" stroke-width="1.3" />
<path d="M40 433C31 499 51 535 81 544M760 433C769 499 749 535 719 544" fill="none" stroke="#56636b" stroke-opacity=".6" stroke-width="1.3" />
<g id="smoky-shell-detail" fill="none" stroke="#85939b" stroke-width="2" opacity=".12">
<circle cx="171" cy="103" r="10" />
<circle cx="629" cy="103" r="10" />
<circle cx="258" cy="355" r="9" />
<circle cx="547" cy="361" r="9" />
<path d="M198 87H246L271 102M602 87H554L529 102M350 262H445L461 279V345H367L350 328ZM274 373H340M474 375H525" />
</g>
<use xlink:href="#stick" transform="translate(190 181)" />
<use xlink:href="#stick" transform="translate(500 306)" />
<g id="minus-button" transform="translate(301 118)">
<use xlink:href="#small-key" />
<path d="M-9-3H9V2H-9Z" fill="#18232b" />
</g>
<g id="plus-button" transform="translate(499 118)">
<use xlink:href="#small-key" />
<path d="M-2.5-12H2.5V-3H11V2H2.5V11H-2.5V2H-11V-3H-2.5Z" fill="#18232b" />
</g>
<use xlink:href="#capture" transform="translate(349 181)" />
<use xlink:href="#home" transform="translate(451 181)" />
<g id="directional-pad" transform="translate(294 306)">
<path d="M-17-62H17Q23-62 23-56V-23H56Q62-23 62-17V17Q62 23 56 23H23V56Q23 62 17 62H-17Q-23 62-23 56V23H-56Q-62 23-62 17V-17Q-62-23-56-23H-23V-56Q-23-62-17-62Z" fill="#152028" />
<path d="M-15-57H15Q18-57 18-54V-23Q18-18 23-18H54Q57-18 57-15V15Q57 18 54 18H23Q18 18 18 23V54Q18 57 15 57H-15Q-18 57-18 54V23Q-18 18-23 18H-54Q-57 18-57 15V-15Q-57-18-54-18H-23Q-18-18-18-23V-54Q-18-57-15-57Z" fill="url(#button)" stroke="#6b7880" stroke-width="1.5" />
<path d="M-4-38L0-46L4-38ZM-4 38L0 46L4 38ZM-38-4L-46 0L-38 4ZM38-4L46 0L38 4Z" fill="#b4bfc5" />
</g>
<g id="player-indicators">
<path d="M356 406H444V416H356Z" fill="#1a252c" />
<rect x="372" y="409" width="8" height="3" rx="1" fill="#99bc76" />
<path d="M388 409H396V412H388ZM404 409H412V412H404ZM420 409H428V412H420Z" fill="#606e76" />
</g>
<g transform="translate(610 138)">
<use xlink:href="#key" />
<path d="M-8-13L8 10M8-13L-8 10" fill="none" stroke="#bdc6cb" stroke-width="2.6" stroke-linejoin="round" />
</g>
<g transform="translate(558 190)">
<use xlink:href="#key" />
<path d="M-9-13L0-1L9-13M0-1V10" fill="none" stroke="#bdc6cb" stroke-width="2.6" stroke-linejoin="round" />
</g>
<g transform="translate(662 190)">
<use xlink:href="#key" />
<path d="M-10 10L0-14L10 10M-6 2H6" fill="none" stroke="#bdc6cb" stroke-width="2.6" stroke-linejoin="round" />
</g>
<g transform="translate(610 242)">
<use xlink:href="#key" />
<path d="M-7-13V10H1C12 10 12-2 1-2H-7M-7-13H1C11-13 11-2 1-2" fill="none" stroke="#bdc6cb" stroke-width="2.6" stroke-linejoin="round" />
</g>
<g id="switch-mark" transform="translate(349 63) scale(.64)" fill="#919da3" opacity=".65">
<g transform="translate(39 0)">
<path d="M36 0H22C9 0 0 10 0 23V59C0 72 9 82 22 82H36ZM29 7V75H22C13 75 7 69 7 59V23C7 13 13 7 22 7Z" fill-rule="evenodd" />
<circle cx="21" cy="25" r="7.8" />
<path d="M46 0H61C74 0 82 10 82 23V59C82 72 74 82 61 82H46ZM64 37A8 8 0 1 0 64 53A8 8 0 1 0 64 37Z" fill-rule="evenodd" />
</g>
<g>
<path aria-label="NINTENDO" d="M26.371 111 21.274 101.994Q21.424 103.305 21.424 104.102V111H19.249V99.304H22.046L27.218 108.385Q27.068 107.132 27.068 106.103V99.304H29.243V111ZM36.526 111V99.304H38.974V111ZM53.371 111 48.274 101.994Q48.424 103.305 48.424 104.102V111H46.249V99.304H49.046L54.218 108.385Q54.068 107.132 54.068 106.103V99.304H56.243V111ZM68.805 101.197V111H66.356V101.197H62.579V99.304H72.59V101.197ZM78.91 111V99.304H88.107V101.197H81.359V104.135H87.601V106.028H81.359V109.107H88.448V111ZM102.371 111 97.274 101.994Q97.424 103.305 97.424 104.102V111H95.249V99.304H98.046L103.218 108.385Q103.068 107.132 103.068 106.103V99.304H105.243V111ZM122.951 105.065Q122.951 106.875 122.242 108.223Q121.532 109.572 120.233 110.286Q118.934 111 117.257 111H112.526V99.304H116.759Q119.714 99.304 121.333 100.794Q122.951 102.284 122.951 105.065ZM120.486 105.065Q120.486 103.181 119.507 102.189Q118.527 101.197 116.709 101.197H114.974V109.107H117.05Q118.627 109.107 119.556 108.02Q120.486 106.933 120.486 105.065ZM141.175 105.098Q141.175 106.924 140.452 108.311Q139.73 109.697 138.385 110.431Q137.041 111.166 135.248 111.166Q132.492 111.166 130.927 109.543Q129.363 107.92 129.363 105.098Q129.363 102.284 130.923 100.707Q132.484 99.13 135.264 99.13Q138.045 99.13 139.61 100.724Q141.175 102.317 141.175 105.098ZM138.676 105.098Q138.676 103.206 137.78 102.131Q136.883 101.056 135.264 101.056Q133.621 101.056 132.724 102.122Q131.828 103.189 131.828 105.098Q131.828 107.024 132.745 108.132Q133.662 109.24 135.248 109.24Q136.891 109.24 137.784 108.161Q138.676 107.082 138.676 105.098Z" />
<path aria-label="SWITCH" d="M33.054 135.26Q33.054 138.696 30.506 140.514Q27.958 142.332 23.027 142.332Q18.528 142.332 15.971 140.738Q13.415 139.145 12.684 135.907L17.416 135.127Q17.897 136.986 19.292 137.825Q20.686 138.663 23.16 138.663Q28.29 138.663 28.29 135.542Q28.29 134.546 27.7 133.898Q27.111 133.251 26.04 132.819Q24.969 132.388 21.931 131.773Q19.308 131.159 18.279 130.786Q17.25 130.412 16.419 129.906Q15.589 129.399 15.008 128.686Q14.427 127.972 14.104 127.009Q13.78 126.046 13.78 124.801Q13.78 121.63 16.162 119.945Q18.544 118.26 23.093 118.26Q27.443 118.26 29.626 119.621Q31.809 120.982 32.44 124.12L27.692 124.768Q27.327 123.257 26.206 122.493Q25.085 121.729 22.994 121.729Q18.544 121.729 18.544 124.519Q18.544 125.432 19.018 126.013Q19.491 126.594 20.42 127Q21.35 127.407 24.189 128.021Q27.559 128.735 29.012 129.341Q30.464 129.947 31.311 130.752Q32.158 131.558 32.606 132.678Q33.054 133.799 33.054 135.26ZM60.897 142H55.086L51.916 128.47Q51.334 126.079 50.936 123.473Q50.538 125.647 50.289 126.785Q50.04 127.922 46.752 142H40.942L34.916 118.608H39.879L43.266 133.716L44.03 137.368Q44.495 135.061 44.935 132.96Q45.375 130.86 48.247 118.608H53.725L56.68 131.06Q57.029 132.454 57.859 137.368L58.274 135.442L59.154 131.624L61.976 118.608H66.94ZM69.748 142V118.608H74.645V142ZM90.252 122.394V142H85.355V122.394H77.801V118.608H97.823V122.394ZM111.886 138.48Q116.319 138.48 118.045 134.031L122.312 135.642Q120.934 139.028 118.27 140.68Q115.605 142.332 111.886 142.332Q106.242 142.332 103.162 139.136Q100.083 135.94 100.083 130.196Q100.083 124.436 103.054 121.348Q106.026 118.26 111.67 118.26Q115.788 118.26 118.377 119.912Q120.967 121.563 122.013 124.768L117.697 125.946Q117.149 124.187 115.547 123.149Q113.945 122.111 111.77 122.111Q108.45 122.111 106.731 124.17Q105.013 126.229 105.013 130.196Q105.013 134.23 106.781 136.355Q108.549 138.48 111.886 138.48ZM141.107 142V131.973H130.914V142H126.016V118.608H130.914V127.922H141.107V118.608H146.004V142Z" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,149 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="540" height="764" viewBox="0 0 540 764" role="img" aria-labelledby="title description">
<title id="title">Wii Remote and Nunchuk — connected white controllers</title>
<desc id="description">Original white Nintendo Wii Remote beside a white Nunchuk, joined by a gently looping cable at the remote expansion connector. The Nunchuk shows its raised analog stick and rear C and Z buttons in a three-quarter view. Transparent background. Simple vector gradients, with no filters, textures, fonts or external assets.</desc>
<metadata>Visual references: https://commons.wikimedia.org/wiki/File:Nintendo-Wii-Remote-wNunchuk.jpg and https://en.wikipedia.org/wiki/Wii_Remote . Independently drawn artwork; no embedded photographs.</metadata>
<defs>
<linearGradient id="white" x1=".1" y1="0" x2=".9" y2="1">
<stop stop-color="#f5f7f9" />
<stop offset="1" stop-color="#d7dee5" />
</linearGradient>
<linearGradient id="side" x1="0" y1="0" x2="1" y2=".6">
<stop stop-color="#e1e7ec" />
<stop offset="1" stop-color="#b9c4cf" />
</linearGradient>
<linearGradient id="button" x1=".2" y1="0" x2=".7" y2="1">
<stop stop-color="#fff" />
<stop offset="1" stop-color="#d3dde6" />
</linearGradient>
<radialGradient id="rubber" cx="45%" cy="30%" r="80%">
<stop stop-color="#eef2f4" />
<stop offset="1" stop-color="#c1cdd5" />
</radialGradient>
<radialGradient id="nunchuk-shell" cx="32%" cy="18%" r="90%">
<stop stop-color="#f5f8fa"/>
<stop offset=".58" stop-color="#dce4ea"/>
<stop offset="1" stop-color="#b3c1cc"/>
</radialGradient>
<linearGradient id="nunchuk-deck" x1=".25" y1="0" x2=".8" y2="1">
<stop stop-color="#ffffff"/>
<stop offset=".65" stop-color="#e9eff3"/>
<stop offset="1" stop-color="#ced9e1"/>
</linearGradient>
<g id="small-button">
<circle cy="1" r="14" fill="#b1bfca" />
<circle cy="-1" r="12.5" fill="url(#button)" stroke="#f9fbfc" stroke-width="1.1" />
</g>
<g id="number-button">
<circle cy="1" r="21" fill="#a5b4c0" />
<circle cy="-1" r="19" fill="url(#button)" stroke="#fbfcfd" stroke-width="1.2" />
</g>
</defs>
<g id="nunchuk-cable" fill="none" stroke-linecap="round">
<path d="M13 504C8 597 43 688 143 718C219 741 321 739 375 718C404 707 420 696 420 676" stroke="#a4b2bd" stroke-width="9"/>
<path d="M13 504C8 597 43 688 143 718C219 741 321 739 375 718C404 707 420 696 420 676" stroke="#e7edf1" stroke-width="6.5"/>
</g>
<g id="expansion-connector">
<path d="M396 615H444V651Q444 664 435 665H405Q396 664 396 651Z" fill="url(#side)" stroke="#96a6b2" stroke-width="1.3" />
<path d="M399 615H441V648Q441 657 434 658H406Q399 657 399 648Z" fill="url(#white)" stroke="#f7f9fb" stroke-width="1.2" />
<path d="M391 629H396V643H391ZM444 629H449V643H444Z" fill="#bac6cf" stroke="#93a4b2" stroke-width="1" />
<path d="M413 663H427V679H413Z" fill="#d6dfe5" stroke="#9cabb7" stroke-width="1" />
<path d="M414 668H426M414 673H426" stroke="#a8b6c1" stroke-width="1" />
</g>
<g id="nunchuk" aria-label="White Nunchuk with a sculpted palm grip and low analog stick" transform="translate(-70 140) scale(.6)">
<g id="rear-buttons" stroke="#9eafbc" stroke-width="1.8">
<path d="M611 257L637 263Q650 266 646 280L641 296Q639 302 631 301L605 292Z" fill="url(#side)"/>
<path d="M591 301L621 309Q636 313 631 327L619 351Q615 360 605 356L581 346Z" fill="url(#side)"/>
<path d="M619 270L638 275M603 315L621 321" fill="none" stroke="#f7fafc" stroke-width="2"/>
</g>
<path d="M154 525C130 511 154 452 193 398C240 331 302 276 365 234C418 198 464 175 506 168C552 160 615 165 643 185C656 196 657 224 649 252C637 295 609 346 588 367C569 371 562 356 541 357C498 357 459 389 430 423C390 470 356 529 307 560C261 589 210 584 178 563C162 553 154 540 154 525Z" fill="url(#nunchuk-shell)" stroke="#9fadb7" stroke-width="2.1"/>
<path d="M154 525C133 507 164 446 198 399C245 334 305 281 369 238C422 202 466 179 508 173C550 167 610 169 639 185C649 191 652 206 649 221C579 242 516 266 457 302C380 348 324 407 263 464C217 507 182 532 154 525Z" fill="url(#nunchuk-deck)" stroke="#c1cdd6" stroke-width="1.5"/>
<path d="M165 512C194 474 218 440 263 397C320 341 382 289 454 254C524 220 581 202 630 196" fill="none" stroke="#fff" stroke-opacity=".65" stroke-width="4" stroke-linecap="round"/>
<path d="M160 531C193 530 225 503 267 465C327 409 384 352 459 308C521 271 584 247 647 228" fill="none" stroke="#aebec9" stroke-width="1.8"/>
<path d="M181 559C218 580 261 579 301 556C337 534 366 499 396 460" fill="none" stroke="#f1f6f9" stroke-opacity=".8" stroke-width="2"/>
<g id="analog-stick" transform="translate(505 178)">
<ellipse cy="2" rx="60" ry="24" fill="#cad5de"/>
<ellipse cy="1" rx="49" ry="19" fill="#a9bac6"/>
<path d="M-27-34H27L29 1C22 14-22 14-29 1Z" fill="url(#white)" stroke="#b4c3cd" stroke-width="1.5"/>
<path d="M-26-30V0Q0 10 26 0" fill="none" stroke="#fff" stroke-width="2"/>
<path d="M-45-42C-44-54-25-58 0-58C25-58 44-54 45-42L49-35C48-22-48-22-49-35Z" fill="url(#side)" stroke="#b1c0cb" stroke-width="1.5"/>
<ellipse cy="-44" rx="43" ry="13" fill="url(#rubber)" stroke="#f8fbfc" stroke-width="1.8"/>
<ellipse cy="-45" rx="34" ry="9" fill="#e1e9ee" stroke="#c4d0d9" stroke-width="1.2"/>
<path d="M-30-49Q0-58 30-49" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
</g>
</g>
<g id="nunchuk-strain-relief" transform="translate(22 485) rotate(25)">
<path d="M-10-13H10V15Q10 21 5 21H-5Q-10 21-10 15Z" fill="url(#side)" stroke="#a0afba" stroke-width="1.1"/>
<path d="M-8-5H8M-8 3H8M-7 11H7" stroke="#a6b6c2" stroke-width="1.3"/>
<path d="M-7-10V15" stroke="#f8fafc" stroke-width="1.1"/>
</g>
<g transform="translate(320 10)">
<g id="wii-remote" aria-label="Original white Wii Remote">
<path d="M44 13H156Q176 13 176 37V585Q176 614 150 614H50Q24 614 24 585V37Q24 13 44 13Z" fill="url(#side)" stroke="#8e9da9" stroke-width="1.5" />
<path d="M49 13H151Q164 13 164 22H36Q36 13 49 13Z" fill="#45545f" />
<path d="M46 19H154Q171 19 171 37V582Q171 601 150 603H50Q29 601 29 582V37Q29 19 46 19Z" fill="url(#white)" stroke="#bdc9d2" stroke-width="1.2" />
<path d="M45 23H155Q167 23 167 38M33 41V579Q33 596 51 598" stroke="#fff" stroke-width="1.7" fill="none" />
<path d="M44 606Q100 611 156 606" fill="none" stroke="#a7b5c0" stroke-width="1.2" />
<g id="power-button" transform="translate(56 50)">
<use xlink:href="#small-button" />
<path d="M-4-6A7 7 0 1 0 4-6M0-9V-1" fill="none" stroke="#ce727b" stroke-width="1.8" stroke-linecap="round" />
</g>
<g id="directional-pad" transform="translate(100 128)">
<path d="M-12-39H12Q17-39 17-34V-17H34Q39-17 39-12V12Q39 17 34 17H17V34Q17 39 12 39H-12Q-17 39-17 34V17H-34Q-39 17-39 12V-12Q-39-17-34-17H-17V-34Q-17-39-12-39Z" fill="#adbcc7" />
<path d="M-11-36H11Q14-36 14-33V-17Q14-14 17-14H33Q36-14 36-11V11Q36 14 33 14H17Q14 14 14 17V33Q14 36 11 36H-11Q-14 36-14 33V17Q-14 14-17 14H-33Q-36 14-36 11V-11Q-36-14-33-14H-17Q-14-14-14-17V-33Q-14-36-11-36Z" fill="url(#button)" stroke="#fff" stroke-width="1.2" />
<path d="M0-28V-20M0 20V28M-28 0H-20M20 0H28" fill="none" stroke="#a9b7c2" stroke-width="2.3" stroke-linecap="round" />
</g>
<g id="a-button" transform="translate(100 217)">
<circle cy="1" r="26" fill="#b1bdc9" stroke="#97a7b5" stroke-width="1.2" />
<circle cy="-1" r="23.5" fill="url(#button)" stroke="#fff" stroke-width="1.4" />
<path d="M-7 8L0-12L7 8M-4 1H4" fill="none" stroke="#96a7b6" stroke-width="1.8" stroke-linejoin="round" />
</g>
<g id="minus-button" transform="translate(60 295)">
<use xlink:href="#small-button" />
<path d="M-5-1H5" stroke="#95a6b5" stroke-width="1.8" />
</g>
<g id="home-button" transform="translate(100 295)">
<use xlink:href="#small-button" />
<path d="M-8-2L0-9L8-2H6V6H-6V-2ZM-2 0V4H2V0Z" fill="#68b9d8" fill-rule="evenodd" />
</g>
<g id="plus-button" transform="translate(140 295)">
<use xlink:href="#small-button" />
<path d="M-5-1H5M0-6V4" stroke="#95a6b5" stroke-width="1.8" />
</g>
<g id="one-button" transform="translate(100 446)">
<use xlink:href="#number-button" />
<path d="M-5-7L1-11V9M-5 9H6" fill="none" stroke="#94a5b4" stroke-width="1.8" stroke-linejoin="round" />
</g>
<g id="two-button" transform="translate(100 496)">
<use xlink:href="#number-button" />
<path d="M-6-7C-3-15 10-12 7-4C5 0-2 4-6 9H8" fill="none" stroke="#94a5b4" stroke-width="1.8" stroke-linejoin="round" />
</g>
<g id="player-indicators">
<rect x="54" y="542" width="9" height="9" rx=".8" fill="#64bce8" stroke="#379aca" stroke-width=".7" />
<path d="M82 542H91V551H82ZM110 542H119V551H110ZM138 542H147V551H138Z" fill="#8496a5" />
</g>
<g id="speaker" fill="#8293a1">
<circle cx="89" cy="342" r="1.4" />
<circle cx="100" cy="342" r="1.4" />
<circle cx="111" cy="342" r="1.4" />
<circle cx="89" cy="353" r="2.1" />
<circle cx="100" cy="353" r="2.1" />
<circle cx="111" cy="353" r="2.1" />
<circle cx="89" cy="364" r="2.1" />
<circle cx="100" cy="364" r="2.1" />
<circle cx="111" cy="364" r="2.1" />
<circle cx="89" cy="375" r="2.1" />
<circle cx="100" cy="375" r="2.1" />
<circle cx="111" cy="375" r="2.1" />
<circle cx="89" cy="386" r="2.1" />
<circle cx="100" cy="386" r="2.1" />
<circle cx="111" cy="386" r="2.1" />
<circle cx="89" cy="397" r="1.4" />
<circle cx="100" cy="397" r="1.4" />
<circle cx="111" cy="397" r="1.4" />
</g>
<path aria-label="POWER" fill="#a1aeba" d="M46.87 73.97Q46.87 74.585 46.468 74.948Q46.067 75.311 45.378 75.311H44.104V77H43.517V72.666H45.341Q46.07 72.666 46.47 73.007Q46.87 73.349 46.87 73.97ZM46.279 73.976Q46.279 73.136 45.27 73.136H44.104V74.847H45.295Q46.279 74.847 46.279 73.976ZM52.351 74.813Q52.351 75.493 52.091 76.003Q51.831 76.514 51.345 76.788Q50.859 77.062 50.197 77.062Q49.53 77.062 49.045 76.791Q48.561 76.52 48.306 76.008Q48.05 75.496 48.05 74.813Q48.05 73.773 48.619 73.187Q49.188 72.601 50.204 72.601Q50.865 72.601 51.351 72.864Q51.837 73.127 52.094 73.629Q52.351 74.13 52.351 74.813ZM51.751 74.813Q51.751 74.004 51.346 73.542Q50.942 73.081 50.204 73.081Q49.459 73.081 49.053 73.536Q48.647 73.992 48.647 74.813Q48.647 75.628 49.058 76.106Q49.468 76.585 50.197 76.585Q50.948 76.585 51.349 76.122Q51.751 75.659 51.751 74.813ZM57.85 77H57.149L56.398 74.247Q56.325 73.988 56.183 73.321Q56.103 73.678 56.048 73.918Q55.992 74.158 55.208 77H54.507L53.23 72.666H53.842L54.62 75.419Q54.759 75.936 54.876 76.483Q54.949 76.145 55.046 75.745Q55.143 75.345 55.9 72.666H56.463L57.217 75.363Q57.389 76.025 57.487 76.483L57.515 76.376Q57.598 76.022 57.65 75.799Q57.703 75.576 58.515 72.666H59.127ZM60.215 77V72.666H63.504V73.146H60.803V74.536H63.319V75.01H60.803V76.52H63.63V77ZM68.031 77 66.905 75.2H65.555V77H64.967V72.666H67.007Q67.739 72.666 68.137 72.993Q68.536 73.321 68.536 73.905Q68.536 74.388 68.254 74.717Q67.973 75.047 67.477 75.133L68.708 77ZM67.945 73.912Q67.945 73.533 67.688 73.335Q67.431 73.136 66.948 73.136H65.555V74.736H66.973Q67.437 74.736 67.691 74.519Q67.945 74.302 67.945 73.912Z" />
<path aria-label="HOME" fill="#a1aeba" d="M93.173 324V321.991H90.829V324H90.242V319.666H90.829V321.499H93.173V319.666H93.761V324ZM99.424 321.813Q99.424 322.493 99.164 323.003Q98.904 323.514 98.418 323.788Q97.932 324.062 97.27 324.062Q96.603 324.062 96.118 323.791Q95.634 323.52 95.378 323.008Q95.123 322.496 95.123 321.813Q95.123 320.773 95.692 320.187Q96.261 319.601 97.276 319.601Q97.938 319.601 98.424 319.864Q98.91 320.127 99.167 320.629Q99.424 321.13 99.424 321.813ZM98.824 321.813Q98.824 321.004 98.419 320.542Q98.015 320.081 97.276 320.081Q96.532 320.081 96.126 320.536Q95.72 320.992 95.72 321.813Q95.72 322.628 96.13 323.106Q96.541 323.585 97.27 323.585Q98.021 323.585 98.422 323.122Q98.824 322.659 98.824 321.813ZM104.477 324V321.108Q104.477 320.629 104.505 320.186Q104.354 320.736 104.234 321.047L103.114 324H102.702L101.567 321.047L101.395 320.524L101.293 320.186L101.302 320.527L101.315 321.108V324H100.792V319.666H101.564L102.717 322.671Q102.779 322.853 102.836 323.06Q102.893 323.268 102.911 323.36Q102.936 323.237 103.014 322.986Q103.093 322.736 103.12 322.671L104.252 319.666H105.006V324ZM106.59 324V319.666H109.878V320.146H107.177V321.536H109.694V322.01H107.177V323.52H110.004V324Z" />
<path aria-label="Wii" fill="#9eabb6" d="M101.224 585H98.106L94.771 572.764Q94.442 571.615 93.813 568.648Q93.458 570.234 93.212 571.301Q92.966 572.367 89.479 585H86.362L80.688 565.736H83.409L86.868 577.973Q87.483 580.27 88.003 582.703Q88.331 581.199 88.762 579.422Q89.192 577.645 92.556 565.736H95.058L98.407 577.727Q99.173 580.666 99.61 582.703L99.733 582.225Q100.103 580.652 100.335 579.661Q100.567 578.67 104.177 565.736H106.897ZM108.866 567.062V564.711H111.327V567.062ZM108.866 585V570.207H111.327V585ZM115.087 567.062V564.711H117.548V567.062ZM115.087 585V570.207H117.548V585Z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,95 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="632" viewBox="0 0 200 632" role="img" aria-labelledby="title description">
<title id="title">Wii Remote — original white controller</title>
<desc id="description">Front-view original white Nintendo Wii Remote, with power, cross directional pad, A, minus, home, plus, speaker, 1 and 2 buttons and blue player indicator. Transparent background. Simple vector gradients, with no filters, textures, fonts or external assets.</desc>
<metadata>Visual references: https://commons.wikimedia.org/wiki/File:Nintendo-Wii-Remote-wNunchuk.jpg and https://en.wikipedia.org/wiki/Wii_Remote . Independently drawn artwork; no embedded photographs.</metadata>
<defs>
<linearGradient id="white" x1=".1" y1="0" x2=".9" y2="1">
<stop stop-color="#f5f7f9" />
<stop offset="1" stop-color="#d7dee5" />
</linearGradient>
<linearGradient id="side" x1="0" y1="0" x2="1" y2=".6">
<stop stop-color="#e1e7ec" />
<stop offset="1" stop-color="#b9c4cf" />
</linearGradient>
<linearGradient id="button" x1=".2" y1="0" x2=".7" y2="1">
<stop stop-color="#fff" />
<stop offset="1" stop-color="#d3dde6" />
</linearGradient>
<g id="small-button">
<circle cy="1" r="14" fill="#b1bfca" />
<circle cy="-1" r="12.5" fill="url(#button)" stroke="#f9fbfc" stroke-width="1.1" />
</g>
<g id="number-button">
<circle cy="1" r="21" fill="#a5b4c0" />
<circle cy="-1" r="19" fill="url(#button)" stroke="#fbfcfd" stroke-width="1.2" />
</g>
</defs>
<g id="wii-remote" aria-label="Original white Wii Remote">
<path d="M44 13H156Q176 13 176 37V585Q176 614 150 614H50Q24 614 24 585V37Q24 13 44 13Z" fill="url(#side)" stroke="#8e9da9" stroke-width="1.5" />
<path d="M49 13H151Q164 13 164 22H36Q36 13 49 13Z" fill="#45545f" />
<path d="M46 19H154Q171 19 171 37V582Q171 601 150 603H50Q29 601 29 582V37Q29 19 46 19Z" fill="url(#white)" stroke="#bdc9d2" stroke-width="1.2" />
<path d="M45 23H155Q167 23 167 38M33 41V579Q33 596 51 598" stroke="#fff" stroke-width="1.7" fill="none" />
<path d="M44 606Q100 611 156 606" fill="none" stroke="#a7b5c0" stroke-width="1.2" />
<g id="power-button" transform="translate(56 50)">
<use xlink:href="#small-button" />
<path d="M-4-6A7 7 0 1 0 4-6M0-9V-1" fill="none" stroke="#ce727b" stroke-width="1.8" stroke-linecap="round" />
</g>
<g id="directional-pad" transform="translate(100 128)">
<path d="M-12-39H12Q17-39 17-34V-17H34Q39-17 39-12V12Q39 17 34 17H17V34Q17 39 12 39H-12Q-17 39-17 34V17H-34Q-39 17-39 12V-12Q-39-17-34-17H-17V-34Q-17-39-12-39Z" fill="#adbcc7" />
<path d="M-11-36H11Q14-36 14-33V-17Q14-14 17-14H33Q36-14 36-11V11Q36 14 33 14H17Q14 14 14 17V33Q14 36 11 36H-11Q-14 36-14 33V17Q-14 14-17 14H-33Q-36 14-36 11V-11Q-36-14-33-14H-17Q-14-14-14-17V-33Q-14-36-11-36Z" fill="url(#button)" stroke="#fff" stroke-width="1.2" />
<path d="M0-28V-20M0 20V28M-28 0H-20M20 0H28" fill="none" stroke="#a9b7c2" stroke-width="2.3" stroke-linecap="round" />
</g>
<g id="a-button" transform="translate(100 217)">
<circle cy="1" r="26" fill="#b1bdc9" stroke="#97a7b5" stroke-width="1.2" />
<circle cy="-1" r="23.5" fill="url(#button)" stroke="#fff" stroke-width="1.4" />
<path d="M-7 8L0-12L7 8M-4 1H4" fill="none" stroke="#96a7b6" stroke-width="1.8" stroke-linejoin="round" />
</g>
<g id="minus-button" transform="translate(60 295)">
<use xlink:href="#small-button" />
<path d="M-5-1H5" stroke="#95a6b5" stroke-width="1.8" />
</g>
<g id="home-button" transform="translate(100 295)">
<use xlink:href="#small-button" />
<path d="M-8-2L0-9L8-2H6V6H-6V-2ZM-2 0V4H2V0Z" fill="#68b9d8" fill-rule="evenodd" />
</g>
<g id="plus-button" transform="translate(140 295)">
<use xlink:href="#small-button" />
<path d="M-5-1H5M0-6V4" stroke="#95a6b5" stroke-width="1.8" />
</g>
<g id="one-button" transform="translate(100 446)">
<use xlink:href="#number-button" />
<path d="M-5-7L1-11V9M-5 9H6" fill="none" stroke="#94a5b4" stroke-width="1.8" stroke-linejoin="round" />
</g>
<g id="two-button" transform="translate(100 496)">
<use xlink:href="#number-button" />
<path d="M-6-7C-3-15 10-12 7-4C5 0-2 4-6 9H8" fill="none" stroke="#94a5b4" stroke-width="1.8" stroke-linejoin="round" />
</g>
<g id="player-indicators">
<rect x="54" y="542" width="9" height="9" rx=".8" fill="#64bce8" stroke="#379aca" stroke-width=".7" />
<path d="M82 542H91V551H82ZM110 542H119V551H110ZM138 542H147V551H138Z" fill="#8496a5" />
</g>
<g id="speaker" fill="#8293a1">
<circle cx="89" cy="342" r="1.4" />
<circle cx="100" cy="342" r="1.4" />
<circle cx="111" cy="342" r="1.4" />
<circle cx="89" cy="353" r="2.1" />
<circle cx="100" cy="353" r="2.1" />
<circle cx="111" cy="353" r="2.1" />
<circle cx="89" cy="364" r="2.1" />
<circle cx="100" cy="364" r="2.1" />
<circle cx="111" cy="364" r="2.1" />
<circle cx="89" cy="375" r="2.1" />
<circle cx="100" cy="375" r="2.1" />
<circle cx="111" cy="375" r="2.1" />
<circle cx="89" cy="386" r="2.1" />
<circle cx="100" cy="386" r="2.1" />
<circle cx="111" cy="386" r="2.1" />
<circle cx="89" cy="397" r="1.4" />
<circle cx="100" cy="397" r="1.4" />
<circle cx="111" cy="397" r="1.4" />
</g>
<path aria-label="POWER" fill="#a1aeba" d="M46.87 73.97Q46.87 74.585 46.468 74.948Q46.067 75.311 45.378 75.311H44.104V77H43.517V72.666H45.341Q46.07 72.666 46.47 73.007Q46.87 73.349 46.87 73.97ZM46.279 73.976Q46.279 73.136 45.27 73.136H44.104V74.847H45.295Q46.279 74.847 46.279 73.976ZM52.351 74.813Q52.351 75.493 52.091 76.003Q51.831 76.514 51.345 76.788Q50.859 77.062 50.197 77.062Q49.53 77.062 49.045 76.791Q48.561 76.52 48.306 76.008Q48.05 75.496 48.05 74.813Q48.05 73.773 48.619 73.187Q49.188 72.601 50.204 72.601Q50.865 72.601 51.351 72.864Q51.837 73.127 52.094 73.629Q52.351 74.13 52.351 74.813ZM51.751 74.813Q51.751 74.004 51.346 73.542Q50.942 73.081 50.204 73.081Q49.459 73.081 49.053 73.536Q48.647 73.992 48.647 74.813Q48.647 75.628 49.058 76.106Q49.468 76.585 50.197 76.585Q50.948 76.585 51.349 76.122Q51.751 75.659 51.751 74.813ZM57.85 77H57.149L56.398 74.247Q56.325 73.988 56.183 73.321Q56.103 73.678 56.048 73.918Q55.992 74.158 55.208 77H54.507L53.23 72.666H53.842L54.62 75.419Q54.759 75.936 54.876 76.483Q54.949 76.145 55.046 75.745Q55.143 75.345 55.9 72.666H56.463L57.217 75.363Q57.389 76.025 57.487 76.483L57.515 76.376Q57.598 76.022 57.65 75.799Q57.703 75.576 58.515 72.666H59.127ZM60.215 77V72.666H63.504V73.146H60.803V74.536H63.319V75.01H60.803V76.52H63.63V77ZM68.031 77 66.905 75.2H65.555V77H64.967V72.666H67.007Q67.739 72.666 68.137 72.993Q68.536 73.321 68.536 73.905Q68.536 74.388 68.254 74.717Q67.973 75.047 67.477 75.133L68.708 77ZM67.945 73.912Q67.945 73.533 67.688 73.335Q67.431 73.136 66.948 73.136H65.555V74.736H66.973Q67.437 74.736 67.691 74.519Q67.945 74.302 67.945 73.912Z" />
<path aria-label="HOME" fill="#a1aeba" d="M93.173 324V321.991H90.829V324H90.242V319.666H90.829V321.499H93.173V319.666H93.761V324ZM99.424 321.813Q99.424 322.493 99.164 323.003Q98.904 323.514 98.418 323.788Q97.932 324.062 97.27 324.062Q96.603 324.062 96.118 323.791Q95.634 323.52 95.378 323.008Q95.123 322.496 95.123 321.813Q95.123 320.773 95.692 320.187Q96.261 319.601 97.276 319.601Q97.938 319.601 98.424 319.864Q98.91 320.127 99.167 320.629Q99.424 321.13 99.424 321.813ZM98.824 321.813Q98.824 321.004 98.419 320.542Q98.015 320.081 97.276 320.081Q96.532 320.081 96.126 320.536Q95.72 320.992 95.72 321.813Q95.72 322.628 96.13 323.106Q96.541 323.585 97.27 323.585Q98.021 323.585 98.422 323.122Q98.824 322.659 98.824 321.813ZM104.477 324V321.108Q104.477 320.629 104.505 320.186Q104.354 320.736 104.234 321.047L103.114 324H102.702L101.567 321.047L101.395 320.524L101.293 320.186L101.302 320.527L101.315 321.108V324H100.792V319.666H101.564L102.717 322.671Q102.779 322.853 102.836 323.06Q102.893 323.268 102.911 323.36Q102.936 323.237 103.014 322.986Q103.093 322.736 103.12 322.671L104.252 319.666H105.006V324ZM106.59 324V319.666H109.878V320.146H107.177V321.536H109.694V322.01H107.177V323.52H110.004V324Z" />
<path aria-label="Wii" fill="#9eabb6" d="M101.224 585H98.106L94.771 572.764Q94.442 571.615 93.813 568.648Q93.458 570.234 93.212 571.301Q92.966 572.367 89.479 585H86.362L80.688 565.736H83.409L86.868 577.973Q87.483 580.27 88.003 582.703Q88.331 581.199 88.762 579.422Q89.192 577.645 92.556 565.736H95.058L98.407 577.727Q99.173 580.666 99.61 582.703L99.733 582.225Q100.103 580.652 100.335 579.661Q100.567 578.67 104.177 565.736H106.897ZM108.866 567.062V564.711H111.327V567.062ZM108.866 585V570.207H111.327V585ZM115.087 567.062V564.711H117.548V567.062ZM115.087 585V570.207H117.548V585Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View file

@ -0,0 +1,97 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" height="552" viewBox="0 0 800 552" role="img" aria-labelledby="title description">
<title id="title">Xbox Wireless Controller — Carbon Black</title>
<desc id="description">Front-view Xbox Series X and Series S wireless controller, with offset analog sticks, a faceted hybrid directional pad, colored ABXY letters, Xbox, view, menu and share buttons. Transparent background. Lightweight vector shapes and simple gradients; no filters, textures, fonts or external images.</desc>
<metadata>Visual reference: https://www.xbox.com/en-US/accessories/controllers/xbox-wireless-controller . Independently drawn vector artwork.</metadata>
<defs>
<linearGradient id="shell" x1="0" y1="0" x2=".8" y2="1">
<stop stop-color="#3c454b" />
<stop offset="1" stop-color="#20272c" />
</linearGradient>
<linearGradient id="face" x1=".2" y1="0" x2=".75" y2="1">
<stop stop-color="#4b555a" />
<stop offset="1" stop-color="#293137" />
</linearGradient>
<linearGradient id="button" x1="0" y1="0" x2=".3" y2="1">
<stop stop-color="#505b62" />
<stop offset="1" stop-color="#303a41" />
</linearGradient>
<radialGradient id="dish" cx="50%" cy="90%" r="100%">
<stop stop-color="#283239" />
<stop offset="1" stop-color="#424f58" />
</radialGradient>
<g id="stick">
<circle cy="2" r="62" fill="#202a31" stroke="#546169" stroke-width="2" />
<circle cy="1" r="52" fill="#141e25" />
<circle cy="-2" r="47" fill="#536169" stroke="#748189" stroke-width="1.5" />
<circle cy="-2" r="39" fill="url(#dish)" stroke="#1b272f" stroke-width="2.5" />
</g>
<g id="key">
<circle cy="2" r="29" fill="#172128" />
<circle cy="-1" r="26.5" fill="url(#button)" stroke="#657179" stroke-width="1.3" />
</g>
<g id="small-key">
<circle cy="1" r="19" fill="#172128" />
<circle cy="-1" r="17" fill="url(#button)" stroke="#657179" stroke-width="1.1" />
</g>
</defs>
<g id="controller">
<g id="shoulders" fill="url(#shell)" stroke="#637078" stroke-width="1.4">
<path d="M130 79C141 46 173 31 216 27L269 49L239 86Z" />
<path d="M670 79C659 46 627 31 584 27L531 49L561 86Z" />
</g>
<path d="M180 47C231 38 312 44 400 45C488 44 569 38 620 47C679 54 697 113 718 176C748 266 776 353 782 418C789 479 770 519 729 534C706 540 676 497 637 453C612 424 598 391 563 390H237C202 391 188 424 163 453C124 497 94 540 71 534C30 519 11 479 18 418C24 353 52 266 82 176C103 113 121 54 180 47Z" fill="url(#face)" stroke="#172128" stroke-width="2" />
<path d="M104 140C121 86 137 59 184 53C242 45 323 49 400 50C477 49 558 45 616 53C663 59 679 86 696 140" fill="none" stroke="#748086" stroke-width="2" />
<path d="M75 532C113 495 151 434 192 405C203 397 217 388 235 387H565C583 388 597 397 608 405C649 434 687 495 725 532" fill="none" stroke="#141e25" stroke-width="3" />
<path d="M82 527C121 484 155 432 195 408M718 527C679 484 645 432 605 408" fill="none" stroke="#58656d" stroke-width="1.2" />
<g id="xbox-button" transform="translate(400 93)">
<circle cy="1" r="32" fill="#19242b" />
<circle r="28" fill="#eff3f3" stroke="#748087" stroke-width="1.2" />
<path d="M-22-17C-15-22-6-19 0-13C6-19 15-22 22-17C13-16 8-11 5-7C14 4 21 16 22 20C11 8 4 0 0-3C-4 0-11 8-22 20C-21 16-14 4-5-7C-8-11-13-16-22-17Z" fill="#303b43" />
</g>
<use xlink:href="#stick" transform="translate(204 179)" />
<use xlink:href="#stick" transform="translate(501 300)" />
<g id="view-button" transform="translate(340 181)">
<use xlink:href="#small-key" />
<path d="M-9-8H3V2H-9ZM-3-3H9V7H-3Z" fill="none" stroke="#a6b1b7" stroke-width="1.6" />
</g>
<g id="menu-button" transform="translate(460 181)">
<use xlink:href="#small-key" />
<path d="M-8-7H8M-8-1H8M-8 5H8" fill="none" stroke="#a6b1b7" stroke-width="1.8" />
</g>
<g id="share-button" transform="translate(400 226)">
<rect x="-24" y="-15" width="48" height="30" rx="14" fill="#162128" />
<rect x="-22" y="-14" width="44" height="26" rx="12" fill="url(#button)" stroke="#66747b" stroke-width="1" />
<path d="M-10-1V6H10V-1M0 1V-9M-5-4L0-9L5-4" fill="none" stroke="#a6b1b7" stroke-width="1.6" stroke-linejoin="round" />
</g>
<g id="hybrid-dpad" transform="translate(295 306)">
<circle cy="2" r="64" fill="#172128" stroke="#627078" stroke-width="1.5" />
<path d="M-24-55H24L55-24V24L24 55H-24L-55 24V-24Z" fill="#343e45" />
<path d="M-24-55H24L19-19H-19Z" fill="#59656c" />
<path d="M24-55L55-24L19-19Z" fill="#3b464e" />
<path d="M55-24V24L19 19V-19Z" fill="#46525a" />
<path d="M55 24L24 55L19 19Z" fill="#29353e" />
<path d="M24 55H-24L-19 19H19Z" fill="#647077" />
<path d="M-24 55L-55 24L-19 19Z" fill="#414d55" />
<path d="M-55 24V-24L-19-19V19Z" fill="#505d65" />
<path d="M-55-24L-24-55L-19-19Z" fill="#414d55" />
<path d="M-19-19H19V19H-19Z" fill="#36424a" />
<path d="M-23-54H23M-54-23V23M-23 54H23" stroke="#859097" stroke-opacity=".5" stroke-width="1.2" fill="none" />
</g>
<g transform="translate(600 132)">
<use xlink:href="#key" />
<path d="M-9-13L0-1L9-13M0-1V10" fill="none" stroke="#e4ce59" stroke-width="2.6" stroke-linejoin="round" />
</g>
<g transform="translate(548 183)">
<use xlink:href="#key" />
<path d="M-8-13L8 10M8-13L-8 10" fill="none" stroke="#55b4e2" stroke-width="2.6" stroke-linejoin="round" />
</g>
<g transform="translate(652 183)">
<use xlink:href="#key" />
<path d="M-7-13V10H1C12 10 12-2 1-2H-7M-7-13H1C11-13 11-2 1-2" fill="none" stroke="#e16d72" stroke-width="2.6" stroke-linejoin="round" />
</g>
<g transform="translate(600 234)">
<use xlink:href="#key" />
<path d="M-10 10L0-14L10 10M-6 2H6" fill="none" stroke="#8ac66e" stroke-width="2.6" stroke-linejoin="round" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View file

@ -0,0 +1,192 @@
"use strict";
// Coordinates are in the supplied SVGs' original viewBoxes, including their
// offsets and nested group translations. A single stage rotates art + targets.
const ControllerLayouts = (() => {
const point = (x, y, label, glyph) => ({ x, y, label, glyph });
const back = (label, glyph) => ({ label, glyph, offArt: true });
const dpad = (x, y, distance) => ({
dpad_up: point(x, y - distance, "D-pad Up", "▲"),
dpad_right: point(x + distance, y, "D-pad Right", "▶"),
dpad_down: point(x, y + distance, "D-pad Down", "▼"),
dpad_left: point(x - distance, y, "D-pad Left", "◀"),
});
const triggers = {
left_trigger: back("Left trigger · rear", "LT"),
right_trigger: back("Right trigger · rear", "RT"),
};
const xbox = {
name: "Xbox Wireless Controller", style: "xbox", asset: "xbox-controller-simple.svg",
viewBox: [0, 0, 800, 552], minWidth: 640,
controls: {
...triggers,
left_shoulder: point(204, 39, "LB", "LB"), right_shoulder: point(596, 39, "RB", "RB"),
left_stick: point(204, 177, "Left stick click", "L3"), right_stick: point(501, 298, "Right stick click", "R3"),
...dpad(295, 306, 37),
north: point(600, 131, "Y", "Y"), east: point(652, 182, "B", "B"),
south: point(600, 233, "A", "A"), west: point(548, 182, "X", "X"),
select: point(340, 180, "View", "View"), start: point(460, 180, "Menu", "Menu"),
capture: point(400, 225, "Share", "Share"), system: point(400, 93, "Xbox", "Xbox"),
},
};
const leftFace = {
left_stick: point(84, 139, "Left stick click", "L3"),
select: point(133, 62.5, "Minus", "−"), capture: point(119, 374, "Capture", "Cap"),
...dpad(84, 278, 39),
};
// The SVG's left/right directional centers are 40 units from its center.
leftFace.dpad_left.x = 44;
leftFace.dpad_right.x = 124;
const rightFace = {
north: point(80, 100, "X", "X"), west: point(40, 139, "Y", "Y"),
east: point(120, 139, "A", "A"), south: point(80, 178, "B", "B"),
right_stick: point(80, 276, "Right stick click", "R3"),
start: point(31, 63, "Plus", "+"), system: point(45, 374, "Home", "Home"),
c: point(45, 430, "C", "C"),
};
const translate = (controls, x, y = 0) => Object.fromEntries(
Object.entries(controls).map(([id, value]) => [id, { ...value, x: value.x + x, y: value.y + y }])
);
const railNote = "Rail SL/SR are source-only extras and also emit the left/right shoulder source in solo mode. The physical L or R button shares that shoulder source. Each source ID is editable once; changing an extra does not disable its shoulder alias.";
const wiiCommon = {
select: point(60, 294, "Minus", "−"), system: point(100, 294, "Home", "Home"),
start: point(140, 294, "Plus", "+"),
};
const wiiVertical = {
...wiiCommon, ...dpad(100, 128, 25),
south: back("B · underside", "B"), east: point(100, 216, "A", "A"),
west: point(100, 445, "1", "1"), north: point(100, 495, "2", "2"),
};
const layouts = {
generic: { ...xbox, name: "Generic controller reference", style: "generic", generic: true },
xbox,
dualsense: {
name: "Sony DualSense", style: "playstation", asset: "ps5-dualsense-simple.svg",
viewBox: [0, 0, 800, 548], minWidth: 640,
controls: {
left_trigger: back("L2 · rear", "L2"), right_trigger: back("R2 · rear", "R2"),
left_shoulder: point(165, 32, "L1", "L1"), right_shoulder: point(635, 32, "R1", "R1"),
left_stick: point(282, 280.3, "Left stick click", "L3"), right_stick: point(518, 280.3, "Right stick click", "R3"),
...dpad(166, 174, 39),
north: point(634, 119, "Triangle", "△"), east: point(688, 173, "Circle", "○"),
south: point(634, 227, "Cross", "×"), west: point(580, 173, "Square", "□"),
select: point(231, 92, "Create", "Create"), start: point(569, 92, "Options", "Opt"),
capture: point(400, 120, "Touchpad click", "TP"), system: point(400, 277, "PS", "PS"),
},
note: "The microphone mute button is not exposed as a remappable source. Rear triggers are listed below the front view.",
},
"switch-pro": {
name: "Nintendo Switch Pro Controller", style: "switch", asset: "switch-pro-controller-simple.svg",
viewBox: [0, 0, 800, 570], minWidth: 640,
controls: {
left_trigger: back("ZL · rear", "ZL"), right_trigger: back("ZR · rear", "ZR"),
left_shoulder: point(205, 32, "L", "L"), right_shoulder: point(595, 32, "R", "R"),
left_stick: point(190, 179, "Left stick click", "L3"), right_stick: point(500, 304, "Right stick click", "R3"),
...dpad(294, 306, 40),
north: point(610, 137, "X", "X"), east: point(662, 189, "A", "A"),
south: point(610, 241, "B", "B"), west: point(558, 189, "Y", "Y"),
select: point(301, 118, "Minus", "−"), start: point(499, 118, "Plus", "+"),
capture: point(349, 180, "Capture", "Cap"), system: point(451, 180, "Home", "Home"),
},
},
"switch2-pro": {
name: "Nintendo Switch 2 Pro Controller", style: "switch", asset: "switch-2-pro-controller-simple.svg",
viewBox: [-16, 160, 1232, 884], minWidth: 720,
controls: {
left_trigger: back("ZL · rear", "ZL"), right_trigger: back("ZR · rear", "ZR"),
gl: back("GL · back button", "GL"), gr: back("GR · back button", "GR"),
left_shoulder: point(290, 193, "L", "L"), right_shoulder: point(910, 193, "R", "R"),
left_stick: point(272, 434, "Left stick click", "L3"), right_stick: point(746, 605, "Right stick click", "R3"),
...dpad(424, 602, 60),
north: point(909, 350, "X", "X"), east: point(1005, 433, "A", "A"),
south: point(909, 517, "B", "B"), west: point(813, 433, "Y", "Y"),
select: point(454, 341, "Minus", "−"), start: point(743, 341, "Plus", "+"),
capture: point(526, 434, "Capture", "Cap"), system: point(672, 435, "Home", "Home"),
c: point(599, 693, "C", "C"),
},
note: "C is on the front. GL and GR are rear controls, not front-face buttons. C, GL and GR are source-only inputs.",
},
"joycon2-pair": {
name: "Joy-Con 2 pair", style: "switch", asset: "switch-2-joycons-connected.svg",
viewBox: [-8, -10, 688, 560], minWidth: 620,
controls: {
...translate(leftFace, 64), ...translate(rightFace, 444),
left_shoulder: back("L · top edge", "L"), right_shoulder: back("R · top edge", "R"),
left_trigger: back("ZL · rear", "ZL"), right_trigger: back("ZR · rear", "ZR"),
left_sl: back("Left SL · rail", "L SL"), left_sr: back("Left SR · rail", "L SR"),
right_sl: back("Right SL · rail", "R SL"), right_sr: back("Right SR · rail", "R SR"),
},
note: "Paired inputs use the left controller's profile. The grip is illustrative, not detected. Rear triggers and rails are not visible in this front view. C and rail inputs are source-only; GL/GR belong to Switch 2 Pro, not Joy-Con 2.",
},
"joycon2-left": {
name: "Joy-Con 2 left · sideways solo", style: "switch", asset: "switch-2-joycon-left.svg",
viewBox: [-17, -10, 200, 560], rotation: -90, minWidth: 560,
controls: {
left_stick: leftFace.left_stick, select: leftFace.select, capture: leftFace.capture,
south: { ...leftFace.dpad_left, label: "Left arrow (solo south)", glyph: "▼" },
east: { ...leftFace.dpad_down, label: "Down arrow (solo east)", glyph: "▶" },
west: { ...leftFace.dpad_up, label: "Up arrow (solo west)", glyph: "◀" },
north: { ...leftFace.dpad_right, label: "Right arrow (solo north)", glyph: "▲" },
left_shoulder: back("L / SL · shoulder alias", "L / SL"), right_shoulder: back("SR · shoulder alias", "SR"),
left_trigger: back("ZL · rear", "ZL"),
left_sl: back("SL · rail extra", "SL extra"), left_sr: back("SR · rail extra", "SR extra"),
},
note: "Solo firmware rotates the directional buttons into face-button sources and keeps the stick on LEFT axes. Arrow names refer to the original upright art. " + railNote,
},
"joycon2-right": {
name: "Joy-Con 2 right · sideways solo", style: "switch", asset: "switch-2-joycon-single.svg",
viewBox: [-19, -10, 200, 560], rotation: 90, minWidth: 560,
controls: {
south: { ...rightFace.east, label: "A (solo south)" }, east: { ...rightFace.north, label: "X (solo east)" },
west: { ...rightFace.south, label: "B (solo west)" }, north: { ...rightFace.west, label: "Y (solo north)" },
left_stick: { ...rightFace.right_stick, label: "Right stick · solo LEFT axes/click", glyph: "L3" },
start: rightFace.start, system: rightFace.system, c: rightFace.c,
left_shoulder: back("SL · shoulder alias", "SL"), right_shoulder: back("R / SR · shoulder alias", "R / SR"),
right_trigger: back("ZR · rear", "ZR"),
right_sl: back("SL · rail extra", "SL extra"), right_sr: back("SR · rail extra", "SR extra"),
},
note: "Solo firmware rotates ABXY sources; the right stick becomes LEFT axes and left-stick click. Output labels do not rotate. When paired, select the left controller's profile to edit the pair. " + railNote,
},
"wii-remote": {
name: "Wii Remote · orientation unknown", style: "switch", asset: "wii-remote-simple.svg",
viewBox: [0, 0, 200, 632], minWidth: 240, maxWidth: 260,
controls: {
...wiiCommon,
...Object.fromEntries(["south", "east", "west", "north", "dpad_up", "dpad_right", "dpad_down", "dpad_left"].map(id => [id, back("Logical " + id.replaceAll("_", " "), id.startsWith("dpad") ? id.slice(5) : id)])),
},
note: "Firmware reports a Wii Remote, not its orientation. Only Minus, Plus and Home can be placed unambiguously. Choose an explicit horizontal/accelerometer or vertical preview to place the other sources. Power is not remappable.",
},
"wii-horizontal": {
name: "Wii Remote · horizontal / accelerometer", style: "switch", asset: "wii-remote-simple.svg",
viewBox: [0, 0, 200, 632], rotation: -90, minWidth: 760,
controls: {
...wiiCommon,
south: point(100, 445, "1", "1"), east: point(100, 495, "2", "2"),
west: point(100, 216, "A", "A"), north: back("B · underside", "B"),
dpad_left: point(100, 103, "D-pad physical Up → Left", "◀"),
dpad_up: point(125, 128, "D-pad physical Right → Up", "▲"),
dpad_right: point(100, 153, "D-pad physical Down → Right", "▶"),
dpad_down: point(75, 128, "D-pad physical Left → Down", "▼"),
},
note: "Explicit orientation preview, not detected: 1→south, 2→east, A→west, B→north. D-pad sources rotate counterclockwise. B is underneath; Power is not remappable.",
},
"wii-vertical": {
name: "Wii Remote · vertical", style: "switch", asset: "wii-remote-simple.svg",
viewBox: [0, 0, 200, 632], minWidth: 240, maxWidth: 260, controls: wiiVertical,
note: "Explicit orientation preview, not detected: B→south, A→east, 1→west, 2→north. D-pad directions stay upright. B is underneath; Power is not remappable.",
},
"wii-nunchuk": {
name: "Wii Remote + Nunchuk", style: "switch", asset: "wii-remote-nunchuk-simple.svg",
viewBox: [0, 0, 540, 764], minWidth: 600,
controls: {
...translate({ ...wiiCommon, ...dpad(100, 128, 25), east: wiiVertical.east }, 320, 10),
south: back("B · Remote underside", "B"),
left_shoulder: point(420, 455, "1", "1"), right_shoulder: point(420, 505, "2", "2"),
west: back("Nunchuk C · rear", "C"), north: back("Nunchuk Z · rear", "Z"),
},
annotations: [{ x: 233, y: 220.4, label: "Stick → RIGHT axes (no click)" }],
note: "Nunchuk stick uses RIGHT axes and has no click. B→south, A→east, 1/2→left/right shoulder, Nunchuk C→west and Z→north. Nunchuk C is a normal face source, not the Switch 2 C extra. Power is not remappable.",
},
};
return layouts;
})();

View file

@ -293,6 +293,7 @@ h4 { font-size: 1rem; }
.panel-heading > p { max-width: 440px; color: var(--muted); font-size: 0.78rem; line-height: 1.55; text-align: right; } .panel-heading > p { max-width: 440px; color: var(--muted); font-size: 0.78rem; line-height: 1.55; text-align: right; }
.controller-mapper { display: grid; grid-template-columns: minmax(0, 1fr) 250px; gap: 18px; align-items: stretch; } .controller-mapper { display: grid; grid-template-columns: minmax(0, 1fr) 250px; gap: 18px; align-items: stretch; }
.controller-canvas { .controller-canvas {
min-width: 0;
padding: 18px; padding: 18px;
overflow: hidden; overflow: hidden;
border: 1px solid rgba(89, 199, 255, 0.24); border: 1px solid rgba(89, 199, 255, 0.24);
@ -300,16 +301,20 @@ h4 { font-size: 1rem; }
background: #090f1d; background: #090f1d;
box-shadow: inset 0 1px rgba(255, 255, 255, 0.04); box-shadow: inset 0 1px rgba(255, 255, 255, 0.04);
} }
.controller-caption { display: flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 27px; } .controller-caption { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; min-height: 27px; }
.controller-caption strong { color: #dfe8ff; font-size: 0.8rem; } .controller-caption strong { color: #dfe8ff; font-size: 0.8rem; }
.controller-live { display: inline-flex; align-items: center; gap: 7px; color: var(--muted); font-size: 0.65rem; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase; } .controller-live { display: inline-flex; align-items: center; gap: 7px; color: var(--amber); font-size: 0.65rem; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; }
.controller-live i { width: 7px; height: 7px; border-radius: 50%; background: var(--mint); box-shadow: 0 0 0 4px rgba(74, 222, 165, 0.09), 0 0 12px var(--mint); } .controller-live[data-state="live"] { color: var(--mint); }
.controller-photo-wrap { position: relative; width: 100%; margin: 10px auto 4px; aspect-ratio: 1.62; transition: aspect-ratio 180ms ease; } .layout-picker { margin-block: 16px 8px; }
.controller-canvas[data-style="switch"] .controller-photo-wrap { aspect-ratio: 1.38; } .layout-picker .select { width: 100%; margin-top: 6px; }
.controller-canvas[data-style="playstation"] .controller-photo-wrap { aspect-ratio: 1.8; } .controller-viewport { max-width: 100%; overflow-x: auto; overflow-y: hidden; padding: 16px 0; scrollbar-color: var(--subtle) var(--surface-2); }
.controller-photo { display: block; width: 100%; height: 100%; object-fit: contain; pointer-events: none; user-select: none; filter: drop-shadow(0 22px 28px rgba(0, 0, 0, 0.46)); } .controller-viewport:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }
.controller-photo-wrap { --diagram-min: 640px; --diagram-max: 900px; position: relative; width: 100%; min-width: var(--diagram-min); max-width: var(--diagram-max); margin: 0 auto; aspect-ratio: var(--diagram-ratio, 800 / 552); }
.controller-stage { position: absolute; top: 50%; left: 50%; width: var(--stage-width, 100%); height: var(--stage-height, 100%); transform: translate(-50%, -50%) rotate(var(--stage-angle, 0deg)); }
.controller-photo { display: block; width: 100%; height: 100%; pointer-events: none; user-select: none; filter: drop-shadow(0 16px 20px rgba(0, 0, 0, 0.46)); }
.controller-annotation { position: absolute; left: var(--x); top: var(--y); max-width: 170px; padding: 6px; transform: translate(-50%, -50%) rotate(var(--label-angle, 0deg)); border: 1px solid var(--muted); border-radius: 6px; color: var(--ink); background: var(--surface-2); font-size: 0.65rem; text-align: center; pointer-events: none; }
.extra-controls { display: flex; flex-wrap: wrap; gap: 9px; margin: 10px 0; } .extra-controls { display: flex; flex-wrap: wrap; gap: 9px; margin: 10px 0; }
.extra-controls button { min-width: 48px; padding: 9px 12px; color: var(--ink); border: 1px solid var(--sky); border-radius: 9px; background: var(--surface-2); font-size: 0.72rem; font-weight: 800; cursor: pointer; } .extra-controls button { min-height: 44px; min-width: 48px; padding: 9px 12px; color: var(--ink); border: 1px solid var(--sky); border-radius: 9px; background: var(--surface-2); font-size: 0.72rem; font-weight: 800; cursor: pointer; }
.extra-controls button.disabled-map { color: var(--muted); border-color: var(--control-line); } .extra-controls button.disabled-map { color: var(--muted); border-color: var(--control-line); }
.extra-controls button.selected, .extra-controls button:focus-visible { outline: 2px solid var(--sky); outline-offset: 3px; } .extra-controls button.selected, .extra-controls button:focus-visible { outline: 2px solid var(--sky); outline-offset: 3px; }
.extra-controls button.pressed { color: #090f1d; background: var(--amber); border-color: var(--amber); } .extra-controls button.pressed { color: #090f1d; background: var(--amber); border-color: var(--amber); }
@ -324,90 +329,28 @@ h4 { font-size: 1rem; }
top: var(--y); top: var(--y);
display: grid; display: grid;
place-items: center; place-items: center;
width: 34px; width: 30px;
height: 34px; height: 30px;
padding: 0; padding: 0;
color: #eff6ff; color: #eff6ff;
border: 2px solid var(--sky); border: 2px solid var(--sky);
border-radius: 50%; border-radius: 50%;
background: rgba(7, 12, 25, 0.9); background: rgba(7, 12, 25, 0.9);
box-shadow: 0 0 0 4px rgba(89, 199, 255, 0.12), 0 5px 16px rgba(0, 0, 0, 0.42); box-shadow: 0 0 0 4px rgba(89, 199, 255, 0.12), 0 5px 16px rgba(0, 0, 0, 0.42);
transform: translate(-50%, -50%); transform: translate(-50%, -50%) rotate(var(--label-angle, 0deg));
font-size: 0.7rem; font-size: 0.59rem;
font-weight: 900; font-weight: 900;
line-height: 1; line-height: 1;
transition: transform 120ms ease, border-color 120ms ease, background 120ms ease, box-shadow 120ms ease; transition: transform 120ms ease, border-color 120ms ease, background 120ms ease, box-shadow 120ms ease;
} }
.controller-hotspots button:hover, .controller-hotspots button:focus-visible { z-index: 2; border-color: var(--blue); transform: translate(-50%, -50%) scale(1.13); } .controller-hotspots button:hover, .controller-hotspots button:focus-visible { z-index: 2; outline: 2px solid var(--blue); outline-offset: 2px; }
.controller-hotspots button.selected { z-index: 3; color: #090a16; border-color: var(--rose); background: var(--rose); box-shadow: 0 0 0 5px rgba(255, 124, 159, 0.18), 0 8px 20px rgba(0, 0, 0, 0.48); transform: translate(-50%, -50%) scale(1.16); } .controller-hotspots button.selected { z-index: 3; color: #090a16; border-color: var(--rose); background: var(--rose); box-shadow: 0 0 0 4px rgba(255, 124, 159, 0.18); }
.controller-hotspots button[data-controller-button$="_trigger"] { width: 60px; height: 30px; border-radius: 11px; } .controller-hotspots button.disabled-map:not(.selected):not(.pressed) { color: var(--subtle); border-color: #56617a; background: rgba(8, 11, 20, 0.9); box-shadow: none; }
.controller-hotspots button.disabled-map { color: var(--subtle); border-color: #56617a; background: rgba(8, 11, 20, 0.9); box-shadow: none; } .controller-hotspots button[data-controller-button^="dpad_"] { width: 26px; height: 26px; font-size: 0.58rem; }
.controller-hotspots button[data-controller-button$="_shoulder"] { width: 60px; height: 30px; border-radius: 11px; } .source-unavailable { color: var(--amber); }
.controller-hotspots button[data-controller-button^="dpad_"] { width: 27px; height: 27px; font-size: 0.58rem; }
.controller-hotspots button[data-controller-button="select"],
.controller-hotspots button[data-controller-button="start"],
.controller-hotspots button[data-controller-button="capture"],
.controller-hotspots button[data-controller-button="system"] { width: 29px; height: 29px; font-size: 0.63rem; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="left_shoulder"] { --x: 22%; --y: 12%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="left_trigger"] { --x: 22%; --y: 4%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="right_trigger"] { --x: 78%; --y: 4%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="right_shoulder"] { --x: 78%; --y: 12%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="left_stick"] { --x: 23%; --y: 43%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="right_stick"] { --x: 63%; --y: 68%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="dpad_up"] { --x: 36%; --y: 62%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="dpad_right"] { --x: 40%; --y: 68%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="dpad_down"] { --x: 36%; --y: 74%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="dpad_left"] { --x: 32%; --y: 68%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="north"] { --x: 77%; --y: 33%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="east"] { --x: 84%; --y: 43%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="south"] { --x: 77%; --y: 54%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="west"] { --x: 70%; --y: 43%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="select"] { --x: 42%; --y: 43%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="start"] { --x: 57%; --y: 43%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="capture"] { --x: 50%; --y: 52%; }
.controller-canvas:is([data-style="generic"], [data-style="xbox"]) [data-controller-button="system"] { --x: 50%; --y: 26%; }
.controller-canvas[data-style="switch"] [data-controller-button="left_shoulder"] { --x: 27%; --y: 8%; }
.controller-canvas[data-style="switch"] [data-controller-button="right_shoulder"] { --x: 73%; --y: 8%; }
.controller-canvas[data-style="switch"] [data-controller-button="left_trigger"] { --x: 27%; --y: 2%; }
.controller-canvas[data-style="switch"] [data-controller-button="right_trigger"] { --x: 73%; --y: 2%; }
.controller-canvas[data-style="switch"] [data-controller-button="left_stick"] { --x: 24%; --y: 30%; }
.controller-canvas[data-style="switch"] [data-controller-button="right_stick"] { --x: 63%; --y: 51%; }
.controller-canvas[data-style="switch"] [data-controller-button="dpad_up"] { --x: 35%; --y: 46%; }
.controller-canvas[data-style="switch"] [data-controller-button="dpad_right"] { --x: 39%; --y: 51%; }
.controller-canvas[data-style="switch"] [data-controller-button="dpad_down"] { --x: 35%; --y: 56%; }
.controller-canvas[data-style="switch"] [data-controller-button="dpad_left"] { --x: 31%; --y: 51%; }
.controller-canvas[data-style="switch"] [data-controller-button="north"] { --x: 77%; --y: 22%; }
.controller-canvas[data-style="switch"] [data-controller-button="east"] { --x: 85%; --y: 32%; }
.controller-canvas[data-style="switch"] [data-controller-button="south"] { --x: 77%; --y: 43%; }
.controller-canvas[data-style="switch"] [data-controller-button="west"] { --x: 69%; --y: 32%; }
.controller-canvas[data-style="switch"] [data-controller-button="select"] { --x: 39%; --y: 21%; }
.controller-canvas[data-style="switch"] [data-controller-button="start"] { --x: 61%; --y: 21%; }
.controller-canvas[data-style="switch"] [data-controller-button="capture"] { --x: 44%; --y: 31%; }
.controller-canvas[data-style="switch"] [data-controller-button="system"] { --x: 57%; --y: 32%; }
.controller-canvas[data-style="playstation"] [data-controller-button="left_shoulder"] { --x: 21%; --y: 12%; }
.controller-canvas[data-style="playstation"] [data-controller-button="right_shoulder"] { --x: 79%; --y: 12%; }
.controller-canvas[data-style="playstation"] [data-controller-button="left_stick"] { --x: 34%; --y: 69%; }
.controller-canvas[data-style="playstation"] [data-controller-button="left_trigger"] { --x: 21%; --y: 3%; }
.controller-canvas[data-style="playstation"] [data-controller-button="right_trigger"] { --x: 79%; --y: 3%; }
.controller-canvas[data-style="playstation"] [data-controller-button="right_stick"] { --x: 66%; --y: 69%; }
.controller-canvas[data-style="playstation"] [data-controller-button="dpad_up"] { --x: 18%; --y: 42%; }
.controller-canvas[data-style="playstation"] [data-controller-button="dpad_right"] { --x: 24%; --y: 50%; }
.controller-canvas[data-style="playstation"] [data-controller-button="dpad_down"] { --x: 18%; --y: 58%; }
.controller-canvas[data-style="playstation"] [data-controller-button="dpad_left"] { --x: 12%; --y: 50%; }
.controller-canvas[data-style="playstation"] [data-controller-button="north"] { --x: 83%; --y: 39%; }
.controller-canvas[data-style="playstation"] [data-controller-button="east"] { --x: 91%; --y: 50%; }
.controller-canvas[data-style="playstation"] [data-controller-button="south"] { --x: 83%; --y: 61%; }
.controller-canvas[data-style="playstation"] [data-controller-button="west"] { --x: 75%; --y: 50%; }
.controller-canvas[data-style="playstation"] [data-controller-button="select"] { --x: 25%; --y: 30%; }
.controller-canvas[data-style="playstation"] [data-controller-button="start"] { --x: 75%; --y: 30%; }
.controller-canvas[data-style="playstation"] [data-controller-button="capture"] { --x: 50%; --y: 36%; }
.controller-canvas[data-style="playstation"] [data-controller-button="system"] { --x: 50%; --y: 72%; }
.controller-hint { margin-top: 4px; color: var(--muted); font-size: 0.72rem; text-align: center; } .controller-hint { margin-top: 4px; color: var(--muted); font-size: 0.72rem; text-align: center; }
.controller-credit { margin-top: 8px; color: var(--subtle); font-size: 0.64rem; text-align: center; } .controller-credit { margin-top: 8px; color: var(--subtle); font-size: 0.64rem; text-align: center; }
.controller-credit a { color: var(--muted); text-decoration-color: rgba(171, 181, 204, 0.45); text-underline-offset: 3px; }
.mapping-inspector { .mapping-inspector {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -110,49 +110,45 @@
<div class="controller-mapper"> <div class="controller-mapper">
<div class="controller-canvas" id="controllerCanvas" data-style="generic"> <div class="controller-canvas" id="controllerCanvas" data-style="generic">
<div class="controller-caption"> <div class="controller-caption">
<span class="controller-live"><i></i> Connected layout</span> <span class="controller-live" id="controllerLayoutStatus">Reference layout</span>
<strong id="controllerModel">Generic controller</strong> <strong id="controllerModel">Generic controller</strong>
</div> </div>
<div class="controller-photo-wrap"> <div class="layout-picker">
<img <label class="field-label" for="controllerLayoutPreview">Auto / layout preview</label>
class="controller-photo" <select class="select" id="controllerLayoutPreview" aria-describedby="controllerLayoutHelp">
id="controllerImage" <option value="auto">Auto · selected owner / live metadata</option>
src="/assets/controller-xbox.svg" <option value="xbox">Preview · Xbox</option>
alt="Connected controller" <option value="dualsense">Preview · DualSense</option>
> <option value="switch-pro">Preview · original Switch Pro</option>
<div class="controller-hotspots" id="controllerHotspots"> <option value="switch2-pro">Preview · Switch 2 Pro</option>
<button type="button" data-controller-button="left_shoulder" aria-label="Left Shoulder">L</button> <option value="joycon2-pair">Preview · Joy-Con 2 pair</option>
<button type="button" data-controller-button="right_shoulder" aria-label="Right Shoulder">R</button> <option value="joycon2-left">Preview · Joy-Con 2 left solo</option>
<button type="button" data-controller-button="left_trigger" aria-label="Left Trigger">LT</button> <option value="joycon2-right">Preview · Joy-Con 2 right solo</option>
<button type="button" data-controller-button="right_trigger" aria-label="Right Trigger">RT</button> <option value="wii-horizontal">Preview · Wii horizontal / accelerometer</option>
<button type="button" data-controller-button="dpad_up" aria-label="D-pad Up">▲</button> <option value="wii-vertical">Preview · Wii vertical</option>
<button type="button" data-controller-button="dpad_right" aria-label="D-pad Right">▶</button> <option value="wii-nunchuk">Preview · Wii Remote + Nunchuk</option>
<button type="button" data-controller-button="dpad_down" aria-label="D-pad Down">▼</button> </select>
<button type="button" data-controller-button="dpad_left" aria-label="D-pad Left">◀</button> <p class="field-help" id="controllerLayoutHelp">Preview changes only this editor's source labels and diagram, never firmware topology or saved mappings.</p>
<button type="button" data-controller-button="north" aria-label="North button">N</button> </div>
<button type="button" data-controller-button="west" aria-label="West button">W</button> <div class="controller-viewport" tabindex="0" role="region" aria-label="Controller diagram; scroll horizontally on narrow screens">
<button type="button" data-controller-button="east" aria-label="East button">E</button> <div class="controller-photo-wrap" id="controllerPhotoWrap">
<button type="button" data-controller-button="south" aria-label="South button">S</button> <div class="controller-stage" id="controllerStage">
<button type="button" data-controller-button="left_stick" aria-label="Left Stick">L3</button> <img class="controller-photo" id="controllerImage" src="/assets/xbox-controller-simple.svg" alt="Generic controller reference">
<button type="button" data-controller-button="right_stick" aria-label="Right Stick">R3</button> <div class="controller-hotspots" id="controllerHotspots"></div>
<button type="button" data-controller-button="select" aria-label="Select">−</button> </div>
<button type="button" data-controller-button="start" aria-label="Start">+</button>
<button type="button" data-controller-button="capture" aria-label="Capture">▣</button>
<button type="button" data-controller-button="system" aria-label="System">⌂</button>
</div> </div>
</div> </div>
<p class="controller-hint">Select a highlighted control on the controller to remap it.</p> <p class="controller-hint">Select a control on the illustration or below it. On narrow screens, swipe sideways or use the Source control menu.</p>
<p class="field-label" id="extraControlsLabel">Switch 2 extra inputs</p> <p class="field-label" id="extraControlsLabel">Off-art controls · rear, rails or unlocated sources</p>
<div class="extra-controls" id="extraControls" role="group" aria-labelledby="extraControlsLabel"></div> <div class="extra-controls" id="extraControls" role="group" aria-labelledby="extraControlsLabel"></div>
<p class="field-help">C, back and rail buttons are source-only controls. Unmapped extra inputs produce no console output.</p> <p class="field-help" id="controllerLayoutNote"></p>
<p class="controller-credit"> <p class="controller-credit">Original vector artwork supplied for Switch Pico Controller Studio.</p>
Image:
<a id="controllerCredit" href="https://github.com/AL2009man/Gamepad-Asset-Pack" target="_blank" rel="noreferrer">Controller artwork by Al. Lopez · MIT</a>
</p>
</div> </div>
<aside class="mapping-inspector"> <aside class="mapping-inspector">
<p class="eyebrow">Selected control</p> <p class="eyebrow">Selected control</p>
<label class="field-label" for="selectedSource">Source control</label>
<select class="select" id="selectedSource" aria-describedby="selectedControlDescription"></select>
<div class="selected-control-mark" id="selectedControlGlyph">S</div> <div class="selected-control-mark" id="selectedControlGlyph">S</div>
<h4 id="selectedControlName">South button</h4> <h4 id="selectedControlName">South button</h4>
<p id="selectedControlDescription">Physical South currently produces South.</p> <p id="selectedControlDescription">Physical South currently produces South.</p>
@ -439,6 +435,7 @@
</dialog> </dialog>
<div class="toast" id="toast" role="status" aria-live="polite"></div> <div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="/playtest.js" defer></script> <script src="/playtest.js" defer></script>
<script src="/assets/controller_layouts.js" defer></script>
<script src="/app.js" defer></script> <script src="/app.js" defer></script>
</body> </body>
</html> </html>

View file

@ -43,6 +43,10 @@ const state = {
previewInputConnected: false, previewInputConnected: false,
playtestRequestActive: false, playtestRequestActive: false,
liveSample: null, liveSample: null,
layoutPreview: "auto",
presentation: null,
presentationKey: "",
diagramKey: "",
playtestShift: { key: "", held: false, active: false }, playtestShift: { key: "", held: false, active: false },
playtestTimer: 0, playtestTimer: 0,
libraryRequestActive: false, libraryRequestActive: false,
@ -69,7 +73,13 @@ const elements = {
controllerCanvas: document.querySelector("#controllerCanvas"), controllerCanvas: document.querySelector("#controllerCanvas"),
controllerImage: document.querySelector("#controllerImage"), controllerImage: document.querySelector("#controllerImage"),
controllerModel: document.querySelector("#controllerModel"), controllerModel: document.querySelector("#controllerModel"),
controllerCredit: document.querySelector("#controllerCredit"), controllerLayoutStatus: document.querySelector("#controllerLayoutStatus"),
controllerLayoutPreview: document.querySelector("#controllerLayoutPreview"),
controllerLayoutHelp: document.querySelector("#controllerLayoutHelp"),
controllerLayoutNote: document.querySelector("#controllerLayoutNote"),
controllerPhotoWrap: document.querySelector("#controllerPhotoWrap"),
controllerStage: document.querySelector("#controllerStage"),
selectedSource: document.querySelector("#selectedSource"),
controllerHotspots: document.querySelector("#controllerHotspots"), controllerHotspots: document.querySelector("#controllerHotspots"),
extraControls: document.querySelector("#extraControls"), extraControls: document.querySelector("#extraControls"),
selectedMapping: document.querySelector("#selectedMapping"), selectedMapping: document.querySelector("#selectedMapping"),
@ -176,11 +186,62 @@ const directionalLabels = {
right_sr: "Right SR", right_sr: "Right SR",
}; };
function matchingLiveSample(sample = state.liveSample) {
const owner = currentOwner();
return Boolean(sample?.connected && owner && sample.owner_key === owner.key &&
(owner.index === 0 || sample.identity_key === owner.key));
}
function currentControllerStyle() { function currentControllerStyle() {
return state.identities[state.identityIndex]?.controller?.style || "generic"; const style = state.presentation?.style || currentOwner()?.controller?.style || "generic";
return style === "wii" ? "switch" : style;
}
function sourceLabel(control) {
return (!state.presentation?.layout.generic && state.presentation?.layout.controls[control]?.label) || controlLabel(control);
}
function sourceGlyph(control) {
return (!state.presentation?.layout.generic && state.presentation?.layout.controls[control]?.glyph) || controlGlyph(control, currentControllerStyle());
}
function sourceAvailable(control) {
return state.presentation?.sources.includes(control) ?? true;
}
function sourceChoices(choices, stored = []) {
return choices.filter(control => sourceAvailable(control) || stored.includes(control));
}
function sourceOptions(selected, choices = state.schema.controls, excluded = []) {
const available = sourceChoices(choices);
if (selected && !available.includes(selected)) available.push(selected);
return `<option value=""${selected === null ? " selected" : ""}>None</option>` +
available.map(control => `<option value="${escapeHtml(control)}"${selected === control ? " selected" : ""}${excluded.includes(control) && control !== selected ? " disabled" : ""}>${escapeHtml(sourceLabel(control))}${sourceAvailable(control) ? "" : " (stored / unavailable)"}</option>`).join("");
}
function syncControllerPresentation() {
const owner = currentOwner();
const live = matchingLiveSample() ? state.liveSample : null;
const controller = live?.controller || owner?.controller || {};
const preview = state.layoutPreview !== "auto";
const layoutId = preview ? state.layoutPreview : controller.layout || "generic";
const layout = ControllerLayouts[layoutId] || ControllerLayouts.generic;
const reported = live?.source_controls || owner?.source_controls || state.schema.controls;
const sources = preview ? Object.keys(layout.controls) : reported.filter(control =>
layout.generic || Object.hasOwn(layout.controls, control));
const topologyKnown = !layoutId.startsWith("joycon2-") || live?.layout === layoutId;
const liveDiagram = Boolean(live && !preview && topologyKnown && layoutId !== "wii-remote" && !layout.generic);
const style = preview ? layout.style : controller.style || layout.style;
const key = [owner?.key, layoutId, style, preview, liveDiagram, Boolean(live), live?.identity_key, controller.model, sources.join(",")].join("|");
if (state.presentationKey === key) return false;
state.presentationKey = key;
state.presentation = { layoutId, layout, sources, preview, liveDiagram, live, style, topologyKnown };
return true;
} }
function controlLabel(control, style = currentControllerStyle()) { function controlLabel(control, style = currentControllerStyle()) {
if (style === "wii") style = "switch";
return state.schema.control_labels?.[style]?.[control] || return state.schema.control_labels?.[style]?.[control] ||
directionalLabels[control] || directionalLabels[control] ||
label(control); label(control);
@ -274,17 +335,26 @@ function clearPlaytest(message, stateName = "waiting") {
.forEach((button) => button.classList.remove("pressed")); .forEach((button) => button.classList.remove("pressed"));
document.querySelectorAll(".curve-marker.visible") document.querySelectorAll(".curve-marker.visible")
.forEach((marker) => marker.classList.remove("visible")); .forEach((marker) => marker.classList.remove("visible"));
if (state.schema && state.profile && syncControllerPresentation()) {
renderButtonMap();
refreshSourceControls();
}
renderCapture(); renderCapture();
} }
function renderPlaytest(sample) { function renderPlaytest(sample) {
state.liveSample = sample; state.liveSample = sample;
if (!sample.connected) { if (!matchingLiveSample(sample)) {
clearPlaytest( clearPlaytest(
"Connect or move a controller to compare its raw input with this draft." sample.connected ? "Live input belongs to another profile owner; this draft and its layout are unchanged." :
"Connect or move the selected controller to compare its raw input with this draft."
); );
return; return;
} }
if (syncControllerPresentation()) {
renderButtonMap();
refreshSourceControls();
}
state.previewInputConnected = true; state.previewInputConnected = true;
const shift = state.profile.shift; const shift = state.profile.shift;
const shiftKey = `${sample.identity_key}:${sample.connection_generation}:${shift.mode}:${shift.modifier}`; const shiftKey = `${sample.identity_key}:${sample.connection_generation}:${shift.mode}:${shift.modifier}`;
@ -336,7 +406,7 @@ function renderPlaytest(sample) {
); );
updateCurveMarker("triggers", "left", sample.triggers.left / 65535); updateCurveMarker("triggers", "left", sample.triggers.left / 65535);
updateCurveMarker("triggers", "right", sample.triggers.right / 65535); updateCurveMarker("triggers", "right", sample.triggers.right / 65535);
const style = sample.controller?.style || currentControllerStyle(); const style = currentControllerStyle();
elements.playtestLeftTriggerLabel.textContent = elements.playtestLeftTriggerLabel.textContent =
controlLabel("left_trigger", style); controlLabel("left_trigger", style);
elements.playtestRightTriggerLabel.textContent = elements.playtestRightTriggerLabel.textContent =
@ -347,15 +417,15 @@ function renderPlaytest(sample) {
elements.controllerCanvas.querySelectorAll("[data-controller-button]") elements.controllerCanvas.querySelectorAll("[data-controller-button]")
.forEach((button) => { .forEach((button) => {
button.classList.toggle( button.classList.toggle(
"pressed", pressed.has(button.dataset.controllerButton) "pressed", state.presentation.liveDiagram && pressed.has(button.dataset.controllerButton)
); );
}); });
elements.playtestExtraInputs.textContent = elements.playtestExtraInputs.textContent =
(sample.extra_buttons || []).map((button) => controlLabel(button, style)).join(", ") || "None"; (sample.extra_buttons || []).map(button => sourceLabel(button)).join(", ") || "None";
elements.playtestMappedButtons.textContent = elements.playtestMappedButtons.textContent =
mapped.buttons.map((button) => controlLabel(button, style)).join(", ") || "None"; mapped.buttons.map((button) => controlLabel(button, style)).join(", ") || "None";
elements.playtestMappingHelp.textContent = elements.playtestMappingHelp.textContent =
`${state.playtestShift.active ? "Shift" : "Base"} layer preview; shortcuts, macros and Turbo are not simulated.`; `${state.playtestShift.active ? "Shift" : "Base"} layer preview; shortcuts, macros and Turbo are not simulated.${state.presentation.liveDiagram ? "" : " Diagram is a reference/preview; physical highlighting is off."}`;
elements.playtestPanel.dataset.state = "live"; elements.playtestPanel.dataset.state = "live";
elements.playtestStatus.textContent = "Live"; elements.playtestStatus.textContent = "Live";
const owner = currentOwner(); const owner = currentOwner();
@ -532,7 +602,7 @@ function persistOwnerKey(key) {
function identitySignature(identities) { function identitySignature(identities) {
return identities.map((entry) => ( return identities.map((entry) => (
`${entry.index}:${entry.key}:${entry.label}:` + `${entry.index}:${entry.key}:${entry.label}:` +
`${entry.controller.model}:${entry.controller.style}` `${entry.controller.model}:${entry.controller.style}:${entry.controller.layout}:${entry.source_controls?.join(",")}`
)).join("|"); )).join("|");
} }
@ -571,6 +641,10 @@ function syncLibraryMetadata(identities, restoreStoredOwner = false) {
elements.activeBadge.hidden = !state.active; elements.activeBadge.hidden = !state.active;
elements.activate.disabled = elements.activate.disabled =
!state.adapterConnected || state.busy || state.active; !state.adapterConnected || state.busy || state.active;
if (state.schema && syncControllerPresentation()) {
renderButtonMap();
refreshSourceControls();
}
} }
return ownerChanged; return ownerChanged;
} }
@ -652,11 +726,7 @@ function modeOptions(modes, selected) {
} }
function modifierOptions(selected, excluded = []) { function modifierOptions(selected, excluded = []) {
const available = currentOwner()?.modifier_controls || state.schema.buttons; return sourceOptions(selected, state.schema.controls, excluded);
return `<option value=""${selected === null ? " selected" : ""}>None</option>` +
state.schema.controls.map((control) =>
`<option value="${control}"${selected === control ? " selected" : ""}${available.includes(control) && !excluded.includes(control) ? "" : " disabled"}>${escapeHtml(controlLabel(control))}${available.includes(control) ? "" : " (unavailable)"}</option>`
).join("");
} }
function renderShortcuts() { function renderShortcuts() {
@ -671,10 +741,8 @@ function renderShortcuts() {
<div class="control-card"> <div class="control-card">
<label for="shortcut-${index}">Profile ${index + 1}${state.profileNames[index] ? ` · ${escapeHtml(state.profileNames[index])}` : ""}</label> <label for="shortcut-${index}">Profile ${index + 1}${state.profileNames[index] ? ` · ${escapeHtml(state.profileNames[index])}` : ""}</label>
<select class="select" id="shortcut-${index}" data-kind="shortcut-selector" data-index="${index}"${shortcuts.modifier === null ? " disabled" : ""}> <select class="select" id="shortcut-${index}" data-kind="shortcut-selector" data-index="${index}"${shortcuts.modifier === null ? " disabled" : ""}>
<option value=""${selector === null ? " selected" : ""}>None</option> ${sourceOptions(selector, state.schema.shortcut_selectors,
${state.schema.shortcut_selectors.map((button) => [shortcuts.modifier, ...shortcuts.profiles.filter((_, other) => other !== index)])}
`<option value="${button}"${selector === button ? " selected" : ""}${button === shortcuts.modifier || shortcuts.profiles.some((item, other) => other !== index && item === button) ? " disabled" : ""}>${escapeHtml(controlLabel(button))}</option>`
).join("")}
</select> </select>
</div>`).join(""); </div>`).join("");
} }
@ -694,8 +762,8 @@ function renderShift() {
elements.shiftMap.innerHTML = [...state.schema.buttons, ...state.schema.extra_buttons].map((button) => { elements.shiftMap.innerHTML = [...state.schema.buttons, ...state.schema.extra_buttons].map((button) => {
const map = state.schema.extra_buttons.includes(button) ? shift.extra_button_map : shift.button_map; const map = state.schema.extra_buttons.includes(button) ? shift.extra_button_map : shift.button_map;
return ` return `
<div class="control-card"> <div class="control-card" data-source-card="${button}"${sourceAvailable(button) ? "" : " hidden"}>
<label for="shift-map-${button}">${escapeHtml(controlLabel(button))} → alternate output</label> <label for="shift-map-${button}"><span data-source-label="${button}">${escapeHtml(sourceLabel(button))}</span> → alternate output</label>
<select class="select" id="shift-map-${button}" data-kind="shift-map" data-name="${button}"${shift.mode === "off" || shift.modifier === null ? " disabled" : ""}>${buttonOptions(map[button], true, state.schema.buttons)}</select> <select class="select" id="shift-map-${button}" data-kind="shift-map" data-name="${button}"${shift.mode === "off" || shift.modifier === null ? " disabled" : ""}>${buttonOptions(map[button], true, state.schema.buttons)}</select>
</div>`; </div>`;
}).join(""); }).join("");
@ -708,24 +776,6 @@ function updateShiftValidity() {
? "Choose a modifier for Hold or Toggle, or turn Shift off." : ""); ? "Choose a modifier for Hold or Toggle, or turn Shift off." : "");
} }
const controllerArtwork = {
generic: {
source: "/assets/controller-xbox.svg",
alt: "Generic Xbox-layout controller artwork",
},
xbox: {
source: "/assets/controller-xbox.svg",
alt: "Xbox controller artwork",
},
switch: {
source: "/assets/controller-switch-pro.svg",
alt: "Nintendo Switch Pro Controller artwork",
},
playstation: {
source: "/assets/controller-dualsense.svg",
alt: "Sony DualSense controller artwork",
},
};
const controllerGlyphs = { const controllerGlyphs = {
generic: { generic: {
@ -804,52 +854,169 @@ function setControlMapping(button, output) {
function renderButtonMap() { function renderButtonMap() {
const owner = state.identities[state.identityIndex]; syncControllerPresentation();
const style = owner?.controller?.style || "generic"; const { layout, layoutId, sources, preview, liveDiagram, live, topologyKnown } = state.presentation;
const artwork = controllerArtwork[style] || controllerArtwork.generic; const style = currentControllerStyle();
const selected = state.selectedButton; const selected = state.selectedButton;
const mappedOutput = getControlMapping(selected); const mappedOutput = getControlMapping(selected);
elements.controllerCanvas.dataset.style = style; elements.controllerCanvas.dataset.style = style;
elements.controllerImage.src = artwork.source; elements.controllerCanvas.dataset.layout = layoutId;
elements.controllerImage.alt = artwork.alt; elements.controllerLayoutStatus.dataset.state = liveDiagram ? "live" : "preview";
elements.controllerModel.textContent = owner?.controller?.model || "Generic controller"; elements.controllerLayoutStatus.textContent = preview ? "Manual layout preview" :
elements.controllerCredit.href = "https://github.com/AL2009man/Gamepad-Asset-Pack"; liveDiagram ? "Live · matching owner" : live ? "Live input · reference diagram" : "Offline · owner reference";
elements.controllerCredit.textContent = "Controller artwork by Al. Lopez · MIT"; elements.controllerModel.textContent = layout.name;
elements.controllerLayoutHelp.textContent = preview
elements.extraControls.innerHTML = state.schema.extra_buttons.map((button) => ? `Manual preview only${live ? `; connected input is ${live.controller.model}` : ""}. No topology or saved mapping is changed. Physical highlighting is off.`
`<button type="button" data-controller-button="${button}">${escapeHtml(controlLabel(button, style))}</button>` : live && !topologyKnown
).join(""); ? "Legacy firmware does not report Joy-Con pair/solo topology. This is an owner reference, not detected solo mode. Choose a preview or update firmware."
elements.controllerCanvas.querySelectorAll("[data-controller-button]").forEach((hotspot) => { : "Auto uses only the selected owner's metadata. Preview changes this editor's source labels and diagram, never firmware topology or saved mappings.";
elements.controllerLayoutNote.textContent = layout.note ||
(layout.generic ? "Generic reference art is not a model identification. Additional reported sources are unlocated, not buttons on this drawing." :
"Rear triggers are listed off-art below the front view. Unavailable stored mappings are retained.");
const diagramKey = `${layoutId}:${sources.join(",")}`;
if (diagramKey !== state.diagramKey) {
const focusedSource = elements.controllerCanvas.contains(document.activeElement)
? document.activeElement.dataset.controllerButton : null;
state.diagramKey = diagramKey;
const [x, y, width, height] = layout.viewBox;
const angle = layout.rotation || 0;
const rotated = Math.abs(angle) === 90;
elements.controllerPhotoWrap.style.setProperty("--diagram-min", `${layout.minWidth}px`);
elements.controllerPhotoWrap.style.setProperty("--diagram-max", `${layout.maxWidth || Math.max(layout.minWidth, 900)}px`);
elements.controllerPhotoWrap.style.setProperty("--diagram-ratio", rotated ? `${height} / ${width}` : `${width} / ${height}`);
elements.controllerStage.style.setProperty("--stage-width", `${rotated ? width / height * 100 : 100}%`);
elements.controllerStage.style.setProperty("--stage-height", `${rotated ? height / width * 100 : 100}%`);
elements.controllerStage.style.setProperty("--stage-angle", `${angle}deg`);
elements.controllerStage.style.setProperty("--label-angle", `${-angle}deg`);
elements.controllerImage.src = `/assets/${layout.asset}`;
elements.controllerImage.alt = `${layout.name} · supplied front-view illustration`;
elements.controllerHotspots.replaceChildren();
elements.extraControls.replaceChildren();
for (const id of sources) {
const control = layout.controls[id];
const hotspot = document.createElement("button");
hotspot.type = "button";
hotspot.dataset.controllerButton = id;
if (control && !control.offArt) {
hotspot.style.setProperty("--x", `${(control.x - x) / width * 100}%`);
hotspot.style.setProperty("--y", `${(control.y - y) / height * 100}%`);
elements.controllerHotspots.append(hotspot);
} else {
elements.extraControls.append(hotspot);
}
}
for (const note of layout.annotations || []) {
const annotation = document.createElement("span");
annotation.className = "controller-annotation";
annotation.textContent = note.label;
annotation.style.setProperty("--x", `${(note.x - x) / width * 100}%`);
annotation.style.setProperty("--y", `${(note.y - y) / height * 100}%`);
elements.controllerHotspots.append(annotation);
}
if (focusedSource) {
const replacement = [...elements.controllerCanvas.querySelectorAll("[data-controller-button]")]
.find(button => button.dataset.controllerButton === focusedSource);
(replacement || elements.selectedSource).focus({ preventScroll: true });
}
}
elements.controllerCanvas.querySelectorAll("[data-controller-button]").forEach(hotspot => {
const button = hotspot.dataset.controllerButton; const button = hotspot.dataset.controllerButton;
const output = getControlMapping(button); const output = getControlMapping(button);
hotspot.textContent = state.schema.extra_buttons.includes(button) ? controlLabel(button, style) : controlGlyph(button, style); hotspot.textContent = hotspot.parentElement === elements.extraControls ? sourceLabel(button) : sourceGlyph(button);
hotspot.classList.toggle("selected", button === selected); hotspot.classList.toggle("selected", button === selected);
hotspot.classList.toggle("disabled-map", output === null); hotspot.classList.toggle("disabled-map", output == null);
hotspot.title = `${controlLabel(button, style)} → ${output === null ? "Disabled" : controlLabel(output, style)}`; if (!liveDiagram) hotspot.classList.remove("pressed");
hotspot.setAttribute("aria-pressed", String(button === selected));
hotspot.title = `${sourceLabel(button)} [${button}] → ${output == null ? "Disabled" : controlLabel(output)}`;
hotspot.setAttribute("aria-label", hotspot.title); hotspot.setAttribute("aria-label", hotspot.title);
hotspot.onclick = () => {
state.selectedButton = button;
stopMacroPreview("Preview stopped: control selection changed.");
renderButtonMap();
elements.controllerCanvas.querySelector(`[data-controller-button="${button}"]`).focus();
};
}); });
elements.selectedSource.innerHTML = sourceOptions(selected);
elements.selectedControlGlyph.textContent = controlGlyph(selected, style); elements.selectedSource.querySelector('option[value=""]')?.remove();
elements.selectedControlName.textContent = controlLabel(selected, style); elements.selectedControlGlyph.textContent = sourceGlyph(selected);
elements.selectedControlDescription.textContent = ( elements.selectedControlName.textContent = sourceLabel(selected);
`Physical ${controlLabel(selected, style)} currently produces ${mappedOutput === null ? "no output" : controlLabel(mappedOutput, style)}.` elements.selectedControlDescription.textContent =
); `${sourceAvailable(selected) ? "Source" : "Stored / unavailable source"} ${sourceLabel(selected)} [${selected}] produces ${mappedOutput == null ? "no output" : controlLabel(mappedOutput)}.${sourceAvailable(selected) ? "" : " Its mapping is retained; change the preview to locate it."}`;
elements.selectedMapping.innerHTML = buttonOptions(mappedOutput, true, state.schema.output_controls, style); elements.selectedMapping.innerHTML = buttonOptions(mappedOutput, true, state.schema.output_controls, style);
elements.selectedMapping.onchange = () => {
setControlMapping(selected, elements.selectedMapping.value || null);
renderButtonMap();
updateDirtyState();
};
if (state.liveSample) renderPlaytest(state.liveSample);
} }
elements.controllerCanvas.addEventListener("click", event => {
const hotspot = event.target.closest("[data-controller-button]");
if (!hotspot || state.busy || captureBlocking()) return;
state.selectedButton = hotspot.dataset.controllerButton;
stopMacroPreview("Preview stopped: control selection changed.");
renderButtonMap();
});
elements.selectedSource.addEventListener("change", () => {
if (state.busy || captureBlocking()) return;
state.selectedButton = elements.selectedSource.value;
renderButtonMap();
});
elements.selectedMapping.addEventListener("change", () => {
if (state.busy || captureBlocking()) return;
setControlMapping(state.selectedButton, elements.selectedMapping.value || null);
renderButtonMap();
updateDirtyState();
});
elements.controllerLayoutPreview.addEventListener("change", () => {
if (state.busy || captureBlocking()) return;
state.layoutPreview = elements.controllerLayoutPreview.value;
renderButtonMap();
refreshSourceControls();
});
// Patch source controls in place on topology changes. Never rerender the editor,
// stop macro playback, replace focused inputs, or mutate hidden draft values.
function refreshSourceControls() {
if (!state.profile) return;
elements.builtinActions.dataset.controllerStyle = currentControllerStyle();
elements.macroControls.dataset.controllerStyle = currentControllerStyle();
const shortcuts = state.profile.shortcuts;
const modifiers = [
[elements.shortcutModifier.querySelector("select"), shortcuts.modifier, shortcuts.profiles],
[elements.shift.querySelector("#shift-modifier"), state.profile.shift.modifier, []],
[elements.macroControls.querySelector("#macro-cancel"), state.profile.macros[state.selectedMacro].cancel, []],
];
for (const [select, selected, excluded] of modifiers) {
if (select) select.innerHTML = sourceOptions(selected, state.schema.controls, excluded);
}
elements.shortcuts.querySelectorAll("select").forEach((select, index) => {
select.innerHTML = sourceOptions(shortcuts.profiles[index], state.schema.shortcut_selectors,
[shortcuts.modifier, ...shortcuts.profiles.filter((_, other) => other !== index)]);
});
document.querySelectorAll("[data-source-card]").forEach(card => {
const available = sourceAvailable(card.dataset.sourceCard);
card.hidden = !available && !card.contains(document.activeElement);
card.classList.toggle("source-unavailable", !available);
});
document.querySelectorAll("[data-source-label]").forEach(node => {
const id = node.dataset.sourceLabel;
node.textContent = sourceLabel(id) + (sourceAvailable(id) ? "" : " (stored / unavailable)");
});
document.querySelectorAll('[data-kind="action-chord"]').forEach(input => {
const id = input.dataset.name;
const card = input.closest("label");
card.hidden = !sourceAvailable(id) && !input.checked && input !== document.activeElement;
card.classList.toggle("source-unavailable", !sourceAvailable(id));
card.querySelector("b").textContent = sourceGlyph(id);
card.querySelector("small").textContent = sourceLabel(id) + (sourceAvailable(id) ? "" : " (stored / unavailable)");
});
elements.shiftMap.querySelectorAll("select").forEach(select => {
const map = state.schema.extra_buttons.includes(select.dataset.name)
? state.profile.shift.extra_button_map : state.profile.shift.button_map;
select.innerHTML = buttonOptions(map[select.dataset.name], true, state.schema.buttons);
});
document.querySelectorAll("[data-output-label]").forEach(node => {
node.textContent = controlLabel(node.dataset.outputLabel);
});
}
elements.form.addEventListener("focusout", event => {
const card = event.target.closest("[data-source-card]");
if (card && !sourceAvailable(card.dataset.sourceCard) && !card.contains(event.relatedTarget)) {
card.hidden = true;
}
});
const analogDefinitions = [ const analogDefinitions = [
["sticks", "left", "Left stick", [ ["sticks", "left", "Left stick", [
["center_x", "Center X", -32768, 32767], ["center_x", "Center X", -32768, 32767],
@ -982,14 +1149,14 @@ function renderTurbo() {
elements.turbo.innerHTML = state.schema.buttons.map((button) => { elements.turbo.innerHTML = state.schema.buttons.map((button) => {
const override = settings.overrides[button]; const override = settings.overrides[button];
const off = state.profile.turbo[button] === "off"; const off = state.profile.turbo[button] === "off";
return `<div class="control-card turbo-card" data-turbo-button="${button}"> return `<div class="control-card turbo-card" data-turbo-button="${button}" data-source-card="${button}"${sourceAvailable(button) ? "" : " hidden"}>
<label for="turbo-${button}">${escapeHtml(controlLabel(button))} · physical source</label> <label for="turbo-${button}"><span data-source-label="${button}">${escapeHtml(sourceLabel(button))}</span> · physical source</label>
<select class="select" id="turbo-${button}" data-kind="turbo" data-name="${button}"> <select class="select" id="turbo-${button}" data-kind="turbo" data-name="${button}">
${modeOptions(state.schema.turbo_modes, state.profile.turbo[button])} ${modeOptions(state.schema.turbo_modes, state.profile.turbo[button])}
</select> </select>
<label class="checkbox-pill turbo-override"> <label class="checkbox-pill turbo-override">
<input type="checkbox" data-kind="turbo-override" data-name="${button}"${override ? " checked" : ""}${off ? " disabled" : ""}> <input type="checkbox" data-kind="turbo-override" data-name="${button}"${override ? " checked" : ""}${off ? " disabled" : ""}>
<span>Override shared settings for ${escapeHtml(controlLabel(button))}</span> <span>Override shared settings for <span data-source-label="${button}">${escapeHtml(sourceLabel(button))}</span></span>
</label> </label>
<div class="turbo-setting-grid">${turboSettingFields(override || settings.defaults, button, off || !override)}</div> <div class="turbo-setting-grid">${turboSettingFields(override || settings.defaults, button, off || !override)}</div>
<p class="field-help" data-turbo-timing></p> <p class="field-help" data-turbo-timing></p>
@ -1010,7 +1177,7 @@ function updateTurboTiming() {
card.classList.toggle("narrow-pulse", tooNarrow); card.classList.toggle("narrow-pulse", tooNarrow);
card.querySelector("[data-turbo-timing]").textContent = card.querySelector("[data-turbo-timing]").textContent =
`${on.toFixed(2)} ms ON / ${off.toFixed(2)} ms OFF${mode === "off" ? " · inactive" : ""}`; `${on.toFixed(2)} ms ON / ${off.toFixed(2)} ms OFF${mode === "off" ? " · inactive" : ""}`;
if (tooNarrow) narrow.push(controlLabel(button)); if (tooNarrow) narrow.push(sourceLabel(button));
}); });
elements.turboTimingNotice.classList.toggle("warning", narrow.length > 0); elements.turboTimingNotice.classList.toggle("warning", narrow.length > 0);
elements.turboTimingNotice.textContent = narrow.length elements.turboTimingNotice.textContent = narrow.length
@ -1032,9 +1199,9 @@ function actionChordCard(action, title, description, selectedButtons, defaults)
const effectiveButtons = inherited ? defaults : selectedButtons; const effectiveButtons = inherited ? defaults : selectedButtons;
const selected = new Set(effectiveButtons); const selected = new Set(effectiveButtons);
const defaultText = inherited const defaultText = inherited
? `Using default: ${defaults.map((button) => controlLabel(button)).join(" + ")}.` ? `Using default: ${defaults.map(button => sourceLabel(button)).join(" + ")}.`
: defaults?.length : defaults?.length
? `Clear every selection to restore ${defaults.map((button) => controlLabel(button)).join(" + ")}.` ? `Clear every selection to restore ${defaults.map(button => sourceLabel(button)).join(" + ")}.`
: "Empty disables this action."; : "Empty disables this action.";
return ` return `
<div class="action-card"> <div class="action-card">
@ -1044,11 +1211,11 @@ function actionChordCard(action, title, description, selectedButtons, defaults)
</div> </div>
<div class="check-grid controller-choices"> <div class="check-grid controller-choices">
${state.schema.controls.map((button) => ` ${state.schema.controls.map((button) => `
<label class="checkbox-pill controller-choice"> <label class="checkbox-pill controller-choice"${sourceAvailable(button) || selected.has(button) ? "" : " hidden"}>
<input type="checkbox" data-kind="action-chord" data-action="${action}" data-name="${button}"${selected.has(button) ? " checked" : ""}> <input type="checkbox" data-kind="action-chord" data-action="${action}" data-name="${button}"${selected.has(button) ? " checked" : ""}>
<span data-button="${button}"> <span data-button="${button}">
<b>${controlGlyph(button, state.identities[state.identityIndex]?.controller?.style || "generic")}</b> <b>${escapeHtml(sourceGlyph(button))}</b>
<small>${controlLabel(button)}</small> <small>${escapeHtml(sourceLabel(button))}${sourceAvailable(button) ? "" : " (stored / unavailable)"}</small>
</span> </span>
</label>`).join("")} </label>`).join("")}
</div> </div>
@ -1665,8 +1832,7 @@ function renderMacro() {
stopMacroPreview("Ready", true); stopMacroPreview("Ready", true);
draggedMacroStep = null; draggedMacroStep = null;
macroNotice(""); macroNotice("");
const controllerStyle = const controllerStyle = currentControllerStyle();
state.identities[state.identityIndex]?.controller?.style || "generic";
elements.builtinActions.dataset.controllerStyle = controllerStyle; elements.builtinActions.dataset.controllerStyle = controllerStyle;
elements.macroControls.dataset.controllerStyle = controllerStyle; elements.macroControls.dataset.controllerStyle = controllerStyle;
const macro = state.profile.macros[state.selectedMacro]; const macro = state.profile.macros[state.selectedMacro];
@ -1711,7 +1877,7 @@ function renderMacro() {
<div class="control-card"> <div class="control-card">
<label for="macro-cancel">Macro ${state.selectedMacro + 1} cancel control</label> <label for="macro-cancel">Macro ${state.selectedMacro + 1} cancel control</label>
<select class="select" id="macro-cancel" data-kind="macro-selector" data-field="cancel"> <select class="select" id="macro-cancel" data-kind="macro-selector" data-field="cancel">
${buttonOptions(macro.cancel, true, state.schema.controls, controllerStyle)} ${sourceOptions(macro.cancel)}
</select> </select>
<div class="macro-playback-fields"> <div class="macro-playback-fields">
<div class="number-field"> <div class="number-field">
@ -1738,7 +1904,7 @@ function renderMacro() {
elements.macroPreviewTitle.textContent = `Macro ${state.selectedMacro + 1} draft preview`; elements.macroPreviewTitle.textContent = `Macro ${state.selectedMacro + 1} draft preview`;
elements.macroPreviewButtons.innerHTML = state.schema.buttons.map((button) => ` elements.macroPreviewButtons.innerHTML = state.schema.buttons.map((button) => `
<span class="preview-button" data-preview-button="${button}"> <span class="preview-button" data-preview-button="${button}">
<b>${escapeHtml(controlLabel(button, controllerStyle))}</b><small>Passthrough</small> <b data-output-label="${button}">${escapeHtml(controlLabel(button, controllerStyle))}</b><small>Passthrough</small>
</span>`).join(""); </span>`).join("");
renderMacroSteps(); renderMacroSteps();
updateMacroPlayback(); updateMacroPlayback();
@ -1795,7 +1961,7 @@ function renderMacroSteps() {
${state.schema.buttons.map((button) => ` ${state.schema.buttons.map((button) => `
<label class="checkbox-pill"> <label class="checkbox-pill">
<input type="checkbox" data-kind="macro-output" data-index="${index}" data-name="${button}"${outputButtons.has(button) ? " checked" : ""}${overrides.has("buttons") ? "" : " disabled"}> <input type="checkbox" data-kind="macro-output" data-index="${index}" data-name="${button}"${outputButtons.has(button) ? " checked" : ""}${overrides.has("buttons") ? "" : " disabled"}>
<span>${controlLabel(button, controllerStyle)}</span> <span data-output-label="${button}">${escapeHtml(controlLabel(button, controllerStyle))}</span>
</label>`).join("")} </label>`).join("")}
</div> </div>
</div> </div>
@ -1832,6 +1998,7 @@ function renderEditor() {
renderFeedback(); renderFeedback();
renderTurbo(); renderTurbo();
renderMacro(); renderMacro();
refreshSourceControls();
elements.loading.hidden = true; elements.loading.hidden = true;
elements.form.hidden = false; elements.form.hidden = false;
updateDirtyState(); updateDirtyState();

View file

@ -1344,6 +1344,12 @@ void test_switch2_pair_lifecycle(bool right_first) {
solo.state.right_stick_x == 0 && solo.state.right_stick_y == 0 && solo.state.right_stick_x == 0 && solo.state.right_stick_y == 0 &&
(right_first ? solo.state.button_south : solo.state.button_west), (right_first ? solo.state.button_south : solo.state.button_west),
"solo JoyCon must rotate face controls, stick click and rail shoulders"); "solo JoyCon must rotate face controls, stick click and rail shoulders");
Bluepad32PlaytestSnapshot playtest{};
bluepad32_input_backend_playtest_snapshot(0, &playtest);
require(playtest.controller_layout ==
(right_first ? Bluepad32ControllerLayout::kJoyCon2RightSolo
: Bluepad32ControllerLayout::kJoyCon2LeftSolo),
"playtest must identify the live rotated solo half");
require(solo.state.left_stick_x == require(solo.state.left_stick_x ==
(right_first ? INT16_MAX : scale_axis(100)) && (right_first ? INT16_MAX : scale_axis(100)) &&
solo.state.left_stick_y == solo.state.left_stick_y ==
@ -1363,6 +1369,11 @@ void test_switch2_pair_lifecycle(bool right_first) {
merged.connection_generation != solo.connection_generation && merged.connection_generation != solo.connection_generation &&
controller_identity_equal(merged.identity, identity_for_device(&left)), controller_identity_equal(merged.identity, identity_for_device(&left)),
"either connection order must merge into first output with left profile owner"); "either connection order must merge into first output with left profile owner");
bluepad32_input_backend_playtest_snapshot(0, &playtest);
require(playtest.controller_layout ==
Bluepad32ControllerLayout::kJoyCon2MergedPair &&
playtest.state.motion_sample_count == 0,
"playtest must detect a live companion before any merged motion report");
Bluepad32CaptureSnapshot capture{}; Bluepad32CaptureSnapshot capture{};
require(bluepad32_input_backend_capture_page(0, 0, &capture) && require(bluepad32_input_backend_capture_page(0, 0, &capture) &&
capture.state == CaptureState::kDisconnected, capture.state == CaptureState::kDisconnected,
@ -1445,6 +1456,11 @@ void test_switch2_pair_lifecycle(bool right_first) {
!detached.state.dpad_up && !detached.state.button_east && !detached.state.dpad_up && !detached.state.button_east &&
(right_first ? detached.state.button_south : detached.state.button_west), (right_first ? detached.state.button_south : detached.state.button_west),
"either half detach must immediately publish only the rotated survivor and own profile"); "either half detach must immediately publish only the rotated survivor and own profile");
bluepad32_input_backend_playtest_snapshot(0, &playtest);
require(playtest.controller_layout ==
(right_first ? Bluepad32ControllerLayout::kJoyCon2RightSolo
: Bluepad32ControllerLayout::kJoyCon2LeftSolo),
"playtest must stop presenting a pair immediately after companion loss");
require(survivor.last_rumble_duration_ms == 0 && require(survivor.last_rumble_duration_ms == 0 &&
slot_snapshot(2).connection_generation == ordinary_before.connection_generation && slot_snapshot(2).connection_generation == ordinary_before.connection_generation &&
slot_snapshot(2).state.button_north, slot_snapshot(2).state.button_north,
@ -2031,6 +2047,29 @@ void test_rejections() {
playtest.battery == 201 && playtest.battery == 201 &&
(playtest.capabilities & 0x08u) != 0, (playtest.capabilities & 0x08u) != 0,
"playtest snapshot did not expose input capabilities"); "playtest snapshot did not expose input capabilities");
struct LayoutCase {
uni_controller_subtype_t subtype;
Bluepad32ControllerLayout layout;
};
const LayoutCase layouts[] = {
{CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL, Bluepad32ControllerLayout::kWiiRemote},
{CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL, Bluepad32ControllerLayout::kWiiRemote},
{CONTROLLER_SUBTYPE_WIIMOTE_ACCEL, Bluepad32ControllerLayout::kWiiRemote},
{CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK, Bluepad32ControllerLayout::kWiiNunchuk},
{CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL, Bluepad32ControllerLayout::kWiiNunchuk},
{CONTROLLER_SUBTYPE_WII_CLASSIC, Bluepad32ControllerLayout::kUnspecified},
{CONTROLLER_SUBTYPE_WIIUPRO, Bluepad32ControllerLayout::kUnspecified},
{CONTROLLER_SUBTYPE_WII_BALANCE_BOARD, Bluepad32ControllerLayout::kUnspecified},
{CONTROLLER_SUBTYPE_WIIMOTE_UDRAW_TABLET, Bluepad32ControllerLayout::kUnspecified},
{CONTROLLER_SUBTYPE_NONE, Bluepad32ControllerLayout::kUnspecified},
};
for (const LayoutCase& expected : layouts) {
slot_zero.controller_subtype = expected.subtype;
bluepad32_input_backend_playtest_snapshot(0, &playtest);
require(playtest.controller_layout == expected.layout &&
playtest.state.motion_sample_count == 3,
"Wii layout must follow the live subtype, not motion or previous extension");
}
bluepad32_input_backend_report_sent(0); bluepad32_input_backend_report_sent(0);
require(read_controller_state(0, &snapshot) && require(read_controller_state(0, &snapshot) &&
snapshot.motion_sample_count == 3, snapshot.motion_sample_count == 3,

View file

@ -82,6 +82,20 @@ enum {
MISC_BUTTON_CAPTURE = 1 << 3, MISC_BUTTON_CAPTURE = 1 << 3,
}; };
typedef enum {
CONTROLLER_SUBTYPE_NONE = 0,
CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL,
CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL,
CONTROLLER_SUBTYPE_WIIMOTE_ACCEL,
CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK,
CONTROLLER_SUBTYPE_UNUSED_00,
CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL,
CONTROLLER_SUBTYPE_WII_CLASSIC,
CONTROLLER_SUBTYPE_WIIUPRO,
CONTROLLER_SUBTYPE_WII_BALANCE_BOARD,
CONTROLLER_SUBTYPE_WIIMOTE_UDRAW_TABLET,
} uni_controller_subtype_t;
struct uni_gamepad_t { struct uni_gamepad_t {
uint32_t dpad; uint32_t dpad;
uint32_t buttons; uint32_t buttons;
@ -148,6 +162,7 @@ inline uint8_t uni_circular_buffer_is_empty(const uni_circular_buffer_t* buffer)
struct uni_hid_device_s { struct uni_hid_device_s {
uint16_t vendor_id; uint16_t vendor_id;
uint16_t product_id; uint16_t product_id;
uni_controller_subtype_t controller_subtype;
uni_bt_conn_t conn; uni_bt_conn_t conn;
int idx; int idx;
bool gamepad; bool gamepad;

View file

@ -137,6 +137,7 @@ class FakeDevice:
self.playtest_state_generation = 93 self.playtest_state_generation = 93
self.playtest_button_mask = 0x9001 self.playtest_button_mask = 0x9001
self.playtest_extra_buttons = 0 self.playtest_extra_buttons = 0
self.playtest_layout = 0
self.playtest_sticks = (-1234, 2345, -30000, 30000) self.playtest_sticks = (-1234, 2345, -30000, 30000)
self.playtest_triggers = (123, 65000) self.playtest_triggers = (123, 65000)
self.playtest_motion = (1, -2, 3, -4, 5, -6) self.playtest_motion = (1, -2, 3, -4, 5, -6)
@ -234,6 +235,7 @@ class FakeDevice:
payload[39] = self.playtest_battery payload[39] = self.playtest_battery
payload[40] = self.playtest_capabilities payload[40] = self.playtest_capabilities
payload[54] = self.playtest_extra_buttons payload[54] = self.playtest_extra_buttons
payload[55] = self.playtest_layout
if self.playtest_motion is not None: if self.playtest_motion is not None:
struct.pack_into("<hhhhhh", payload, 42, *self.playtest_motion) struct.pack_into("<hhhhhh", payload, 42, *self.playtest_motion)
return bytes(payload), flags return bytes(payload), flags
@ -2103,25 +2105,45 @@ def test_profile_playtest_decodes_raw_controller_state() -> None:
config_manager.parse_profile_playtest(envelope) config_manager.parse_profile_playtest(envelope)
def test_playtest_extra_inputs_and_legacy_firmware_are_distinct() -> None: def test_playtest_layout_and_extra_inputs_preserve_legacy_firmware() -> None:
device = FakeDevice() device = FakeDevice()
device.stable_identity = replace(device.stable_identity, vendor_id=0x057E, product_id=0x2067)
device.playtest_extra_buttons = 0x55 device.playtest_extra_buttons = 0x55
device.playtest_layout = 3
payload, flags = device._profile_playtest_payload() payload, flags = device._profile_playtest_payload()
def parse(data: bytes, schema: int) -> config_manager.ProfilePlaytest: def parse(data: bytes, schema: int) -> config_manager.ProfilePlaytest:
return config_manager.parse_profile_playtest(config_manager.parse_response( return config_manager.parse_profile_playtest(config_manager.parse_response(
make_response(config_manager.OP_PROFILE_PLAYTEST, data, flags=flags, schema=schema), make_response(config_manager.OP_PROFILE_PLAYTEST, data, flags=flags, schema=schema),
config_manager.OP_PROFILE_PLAYTEST, config_manager.OP_PROFILE_PLAYTEST,
)) ))
current = parse(payload, 3)
current = parse(payload, 4)
assert current.to_json_object()["layout"] == "joycon2-pair"
assert current.to_json_object()["extra_buttons"] == ["c", "gr", "left_sr", "right_sr"] assert current.to_json_object()["extra_buttons"] == ["c", "gr", "left_sr", "right_sr"]
assert current.to_json_object()["buttons"] == ["south", "dpad_up", "dpad_right"] assert current.to_json_object()["buttons"] == ["south", "dpad_up", "dpad_right"]
legacy = parse(payload[:54], 2) assert parse(payload[:55], 3) == replace(current, layout=None)
assert legacy == replace(current, extra_buttons=0) assert parse(payload[:54], 2) == replace(current, extra_buttons=0, layout=None)
assert legacy.to_json_object()["extra_buttons"] == [] assert parse(payload[:55] + b"\x00", 4).layout is None
for data, schema in (
(payload[:54] + b"\x80", 3),
(payload[:54] + b"\x80\x03", 4),
(payload[:55] + b"\x06", 4),
(payload[:55] + b"\xff", 4),
(payload, 2),
(payload, 3),
(payload[:55], 4),
(payload + b"\x00", 4),
(payload, 5),
):
with pytest.raises(config_manager.ConfigManagerError):
parse(data, schema)
device.playtest_connected = False
disconnected, flags = device._profile_playtest_payload()
assert parse(disconnected, 4).layout is None
with pytest.raises(config_manager.ConfigManagerError): with pytest.raises(config_manager.ConfigManagerError):
parse(payload[:54] + b"\x80", 3) parse(disconnected[:55] + b"\x03", 4)
with pytest.raises(config_manager.ConfigManagerError):
parse(payload, 2)
def test_profile_reset_and_activate_wait_for_correlated_transactions( def test_profile_reset_and_activate_wait_for_correlated_transactions(

View file

@ -68,11 +68,35 @@ def test_editor_serves_assets_and_complete_schema(
with urllib.request.urlopen(f"{base_url}/app.js", timeout=2) as response: with urllib.request.urlopen(f"{base_url}/app.js", timeout=2) as response:
response.read() response.read()
assert response.headers["Content-Type"].startswith("text/javascript") assert response.headers["Content-Type"].startswith("text/javascript")
with urllib.request.urlopen( for filename in (
f"{base_url}/assets/controller-switch-pro.svg", timeout=2 "switch-pro-controller-simple.svg",
) as response: "switch-2-pro-controller-simple.svg",
response.read() "switch-2-joycon-left.svg",
assert response.headers["Content-Type"] == "image/svg+xml" "switch-2-joycon-single.svg",
"switch-2-joycons-connected.svg",
"ps5-dualsense-simple.svg",
"xbox-controller-simple.svg",
"wii-remote-simple.svg",
"wii-remote-nunchuk-simple.svg",
):
with urllib.request.urlopen(
f"{base_url}/assets/{filename}", timeout=2
) as response:
assert response.headers["Content-Type"] == "image/svg+xml"
assert b"<svg" in response.read()
for path in (
"/assets/controller-switch-pro.svg",
"/assets/controller-dualsense.svg",
"/assets/controller-xbox.svg",
"/assets/GAMEPAD_ASSET_LICENSE.txt",
"/assets/../profile_editor.html",
"/assets/%2e%2e/profile_editor.html",
"/assets/%2e%2e%2fprofile_editor.html",
"/assets/switch-2-joycon-left.svg/../profile_editor.html",
):
with pytest.raises(urllib.error.HTTPError) as error:
urllib.request.urlopen(f"{base_url}{path}", timeout=2)
assert error.value.code == 404
status, schema = request_json(f"{base_url}/api/schema") status, schema = request_json(f"{base_url}/api/schema")
@ -91,9 +115,13 @@ def test_editor_serves_assets_and_complete_schema(
) )
@pytest.mark.parametrize("product_id", [0x2069, 0x2067, 0x2066]) @pytest.mark.parametrize(("product_id", "extras"), [
(0x2069, {"c", "gl", "gr"}),
(0x2067, {"left_sl", "left_sr"}),
(0x2066, {"c", "right_sl", "right_sr"}),
])
def test_switch2_input_choices_are_never_output_targets( def test_switch2_input_choices_are_never_output_targets(
monkeypatch: pytest.MonkeyPatch, product_id: int, monkeypatch: pytest.MonkeyPatch, product_id: int, extras: set[str],
) -> None: ) -> None:
device = FakeDevice() device = FakeDevice()
identity = replace(device.stable_identity, vendor_id=0x057E, product_id=product_id) identity = replace(device.stable_identity, vendor_id=0x057E, product_id=product_id)
@ -106,7 +134,8 @@ def test_switch2_input_choices_are_never_output_targets(
assert status == 200 assert status == 200
owner = listing["identities"][1] owner = listing["identities"][1]
assert owner["controller"]["style"] == "switch" assert owner["controller"]["style"] == "switch"
assert set(config_manager.EXTRA_BUTTONS) <= set(owner["modifier_controls"]) assert set(owner["source_controls"]) & set(config_manager.EXTRA_BUTTONS) == extras
assert set(owner["modifier_controls"]) & set(config_manager.EXTRA_BUTTONS) == extras
assert set(schema["extra_buttons"]).isdisjoint(schema["output_controls"]) assert set(schema["extra_buttons"]).isdisjoint(schema["output_controls"])
assert schema["output_controls"] == list(config_manager.OUTPUT_CONTROLS) assert schema["output_controls"] == list(config_manager.OUTPUT_CONTROLS)
@ -194,23 +223,85 @@ def test_editor_identifies_connected_controller_artwork(
status, listing = request_json(f"{base_url}/api/profiles") status, listing = request_json(f"{base_url}/api/profiles")
assert status == 200 assert status == 200
assert [identity["controller"] for identity in listing["identities"]] == [ assert [
{"model": "Generic controller", "style": "generic"}, (identity["controller"]["style"], identity["controller"]["layout"])
{"model": "Nintendo Switch Pro Controller", "style": "switch"}, for identity in listing["identities"]
{"model": "Sony DualSense", "style": "playstation"}, ] == [
{"model": "Xbox controller", "style": "xbox"}, ("generic", "generic"), ("switch", "switch-pro"),
] ("playstation", "dualsense"), ("xbox", "xbox"),
assert [identity["label"] for identity in listing["identities"]] == [
"Default profile",
"Switch Pro · 05:06",
"DualSense · 15:16",
"Xbox · 50:60",
] ]
assert [identity["key"] for identity in listing["identities"]] == [ assert [identity["key"] for identity in listing["identities"]] == [
identity.to_bytes().hex() for identity in device.profile_identities identity.to_bytes().hex() for identity in device.profile_identities
] ]
@pytest.mark.parametrize("owner_index", [0, 1])
def test_live_layout_transitions_do_not_infer_topology_from_profile_owner(
monkeypatch: pytest.MonkeyPatch, owner_index: int,
) -> None:
device = FakeDevice()
left = replace(device.stable_identity, vendor_id=0x057E, product_id=0x2067)
device.stable_identity = left
device.profile_identities = [device.global_identity, left]
device.active_profiles[left.to_bytes()] = 0
device.profiles[(left.to_bytes(), 0)] = config_manager.ControllerProfile.default().to_bytes()
device.playtest_motion = None
with running_server(monkeypatch, device) as (base_url, _):
for code, expected in ((1, "joycon2-left"), (3, "joycon2-pair"), (1, "joycon2-left")):
device.playtest_layout = code
status, sample = request_json(f"{base_url}/api/profiles/{owner_index}/1/playtest")
assert status == 200
assert sample["controller"]["layout"] == expected
assert sample["owner_key"] == device.profile_identities[owner_index].to_bytes().hex()
assert sample["identity_key"] == left.to_bytes().hex()
expected_extras = (
{"c", "left_sl", "left_sr", "right_sl", "right_sr"}
if code == 3 else {"left_sl", "left_sr"}
)
assert set(sample["source_controls"]) & set(config_manager.EXTRA_BUTTONS) == expected_extras
status, listing = request_json(f"{base_url}/api/profiles")
assert status == 200
assert listing["identities"][1]["controller"]["layout"] == "joycon2-left"
device.playtest_connected = False
status, offline = request_json(f"{base_url}/api/profiles/{owner_index}/1/playtest")
assert status == 200
assert offline["connected"] is False
assert offline["layout"] is None
assert offline["controller"] == listing["identities"][owner_index]["controller"]
def test_live_metadata_cannot_turn_an_unrelated_identity_into_a_pair(
monkeypatch: pytest.MonkeyPatch,
) -> None:
device = FakeDevice()
device.playtest_layout = 3
with running_server(monkeypatch, device) as (base_url, _):
status, sample = request_json(f"{base_url}/api/profiles/0/1/playtest")
assert status == 200
assert sample["controller"]["layout"] == "xbox"
def test_wii_pid_does_not_claim_a_remote_or_extension_without_live_metadata(
monkeypatch: pytest.MonkeyPatch,
) -> None:
device = FakeDevice()
device.stable_identity = replace(device.stable_identity, vendor_id=0x057E, product_id=0x0330)
device.profile_identities = [device.global_identity, device.stable_identity]
device.active_profiles[device.stable_identity.to_bytes()] = 0
with running_server(monkeypatch, device) as (base_url, _):
status, listing = request_json(f"{base_url}/api/profiles")
assert status == 200
assert listing["identities"][1]["controller"]["layout"] == "generic"
for code, expected in ((0, "generic"), (4, "wii-remote"), (5, "wii-nunchuk"), (0, "generic")):
device.playtest_layout = code
status, sample = request_json(f"{base_url}/api/profiles/0/1/playtest")
assert status == 200
assert sample["controller"]["layout"] == expected
if code:
assert set(sample["source_controls"]).isdisjoint(config_manager.EXTRA_BUTTONS)
assert ("left_shoulder" in sample["source_controls"]) == (code == 5)
def test_editor_reads_writes_and_activates_profiles_atomically( def test_editor_reads_writes_and_activates_profiles_atomically(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
@ -222,19 +313,12 @@ def test_editor_reads_writes_and_activates_profiles_atomically(
assert listing["identities"][1]["key"] == ( assert listing["identities"][1]["key"] == (
device.stable_identity.to_bytes().hex() device.stable_identity.to_bytes().hex()
) )
assert listing["identities"][1]["controller"] == { assert listing["identities"][1]["controller"]["layout"] == "xbox"
"model": "Xbox controller",
"style": "xbox",
}
status, playtest = request_json(f"{base_url}/api/profiles/1/8/playtest") status, playtest = request_json(f"{base_url}/api/profiles/1/8/playtest")
assert status == 200 assert status == 200
assert playtest["connected"] is True assert playtest["connected"] is True
assert playtest["label"] == "Xbox · 50:60" assert playtest["controller"]["layout"] == "xbox"
assert playtest["controller"] == {
"model": "Xbox controller",
"style": "xbox",
}
assert playtest["buttons"] == [ assert playtest["buttons"] == [
"south", "south",
"dpad_up", "dpad_up",

View file

@ -518,6 +518,8 @@ void test_profile_vendor_requests() {
current_playtest[2].identity = expected_identity; current_playtest[2].identity = expected_identity;
current_playtest[2].physical_button_mask = 0x8001; current_playtest[2].physical_button_mask = 0x8001;
current_playtest[2].state.extra_buttons = 0x7f; current_playtest[2].state.extra_buttons = 0x7f;
current_playtest[2].controller_layout =
Bluepad32ControllerLayout::kJoyCon2MergedPair;
current_playtest[2].state.left_stick_x = -1234; current_playtest[2].state.left_stick_x = -1234;
current_playtest[2].state.left_stick_y = 2345; current_playtest[2].state.left_stick_y = 2345;
current_playtest[2].state.right_stick_x = INT16_MIN; current_playtest[2].state.right_stick_x = INT16_MIN;
@ -558,14 +560,24 @@ void test_profile_vendor_requests() {
control_payload, kResponseHeaderSize + 52)) == -6 && control_payload, kResponseHeaderSize + 52)) == -6 &&
control_payload[kResponseHeaderSize + 39] == 201 && control_payload[kResponseHeaderSize + 39] == 201 &&
control_payload[kResponseHeaderSize + 40] == 0x0f && control_payload[kResponseHeaderSize + 40] == 0x0f &&
control_payload[kResponseHeaderSize + 54] == 0x7f, control_payload[kResponseHeaderSize + 54] == 0x7f &&
control_payload[kResponseHeaderSize + 55] == 3,
"profile playtest response lost live controller state"); "profile playtest response lost live controller state");
current_playtest[2].controller_layout =
static_cast<Bluepad32ControllerLayout>(6);
uint8_t invalid_playtest[kMaximumResponseSize]{};
require(encode_profile_playtest(
2, current_playtest[2], invalid_playtest, sizeof(invalid_playtest)) == 0,
"unknown playtest layout metadata must be rejected");
current_playtest[2].controller_layout =
Bluepad32ControllerLayout::kJoyCon2MergedPair;
current_playtest[2].active = false; current_playtest[2].active = false;
require(usb_configuration_management_vendor_control( require(usb_configuration_management_vendor_control(
0, CONTROL_STAGE_SETUP, &request) && 0, CONTROL_STAGE_SETUP, &request) &&
control_payload[kResponseHeaderSize] == 0 && control_payload[kResponseHeaderSize] == 0 &&
control_payload[kResponseHeaderSize + 1] == 0xff && control_payload[kResponseHeaderSize + 1] == 0xff &&
control_payload[kResponseHeaderSize + 54] == 0, control_payload[kResponseHeaderSize + 54] == 0 &&
control_payload[kResponseHeaderSize + 55] == 0,
"disconnected profile playtest was not encoded"); "disconnected profile playtest was not encoded");
current_profile_metadata = {}; current_profile_metadata = {};
current_profile_metadata.metadata.state = ProfileServiceState::kReady; current_profile_metadata.metadata.state = ProfileServiceState::kReady;