├── .gitignore
├── Qml
├── Back.qml
├── Groove.qml
├── Handle.qml
├── Joystick.qml
├── Lights.qml
├── Main.qml
└── SparseLight.qml
├── QmlJoystick.pro
├── QmlJoystick.qrc
├── README.md
└── Source
└── Main.cpp
/.gitignore:
--------------------------------------------------------------------------------
1 | *.user
2 | build
--------------------------------------------------------------------------------
/Qml/Back.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.0
2 | import QtGraphicalEffects 1.0
3 |
4 | Canvas {
5 | property double radius: 512
6 | property string color: "#34393f"
7 |
8 | id: root
9 | width: 2 * radius
10 | height: 2 * radius
11 | antialiasing: true
12 | smooth: true
13 | contextType: '2d'
14 | onPaint: {
15 | if (context) {
16 | context.reset()
17 | context.beginPath()
18 | context.arc(radius, radius, 0.99 * radius, 0, 2 * Math.PI)
19 |
20 | let gradient = context.createLinearGradient(0, 0, width, height)
21 | gradient.addColorStop(0, Qt.darker(root.color, 1.25))
22 | gradient.addColorStop(1, Qt.lighter(root.color, 1.25))
23 |
24 | context.fillStyle = gradient
25 | context.fill()
26 | }
27 | }
28 |
29 | layer.enabled: false
30 | layer.effect: InnerShadow {
31 | color: "#80000000"
32 | radius: 64 / 512 * root.radius
33 | samples: 32
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Qml/Groove.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.0
2 | import QtGraphicalEffects 1.0
3 |
4 | Canvas {
5 | property double radius: 356
6 | property string color: "#34393f"
7 |
8 | id: root
9 | width: 2 * radius
10 | height: 2 * radius
11 | antialiasing: true
12 | smooth: true
13 | contextType: '2d'
14 | onPaint: {
15 | if (context) {
16 | context.reset()
17 | context.beginPath()
18 | context.arc(radius, radius, 0.98 * radius, 0, 2 * Math.PI)
19 | context.fillStyle = root.color
20 | context.fill()
21 | context.strokeStyle = Qt.lighter(root.color, 1.25)
22 | context.lineWidth = 6 / 356 * radius
23 | context.stroke()
24 | }
25 | }
26 |
27 | layer.enabled: true
28 | layer.effect: DropShadow {
29 | color: "#40000000"
30 | horizontalOffset: 24 / 356 * root.radius
31 | verticalOffset: 24 / 356 * root.radius
32 | radius: 32 / 356 * root.radius
33 | samples: 32
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Qml/Handle.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.0
2 | import QtGraphicalEffects 1.0
3 |
4 | Item {
5 | property double radius: 230
6 | property string color: "#34393f"
7 | property string lightColor: "#8bfcee"
8 |
9 | id: root
10 | width: 2 * radius
11 | height: 2 * radius
12 |
13 | Rectangle {
14 | anchors.centerIn: root
15 | width: 2 * radius
16 | height: 2 * radius
17 | radius: 0.94 * root.radius
18 | color: root.color
19 | layer.enabled: true
20 | layer.effect: DropShadow {
21 | color: "#80000000"
22 | radius: 48 / 230 * root.radius
23 | samples: 32
24 | }
25 | }
26 |
27 | Canvas {
28 | x: 0
29 | y: 0
30 | width: 2 * radius
31 | height: 2 * radius
32 | antialiasing: true
33 | smooth: true
34 | contextType: '2d'
35 | onPaint: {
36 | if (context) {
37 | context.reset()
38 | context.beginPath()
39 | context.arc(radius, radius, 228 / 230 * radius, 0, 2 * Math.PI)
40 | context.fillStyle = root.color
41 | context.fill()
42 |
43 | context.strokeStyle = Qt.lighter(root.color, 1.25)
44 | context.lineWidth = 2 / 230 * radius
45 | context.stroke()
46 |
47 | context.lineWidth = 6 / 230 * radius
48 | context.lineJoin = "round"
49 |
50 | for (let i = 0; i < 4; i++) {
51 | context.translate(radius, radius)
52 | context.rotate(i * Math.PI / 2)
53 | context.translate(-radius, -radius)
54 |
55 | context.beginPath()
56 | context.moveTo(214 / 230 * radius, 64 / 230 * radius)
57 | context.lineTo(230 / 230 * radius, 48 / 230 * radius)
58 | context.lineTo(246 / 230 * radius, 64 / 230 * radius)
59 | context.shadowColor = lightColor
60 | context.shadowBlur = 8 / 230 * radius
61 | context.shadowOffsetX = 0
62 | context.shadowOffsetY = 0
63 | context.strokeStyle = lightColor
64 | context.stroke()
65 | }
66 | }
67 | }
68 |
69 | layer.enabled: true
70 | layer.effect: InnerShadow {
71 | color: "#80000000"
72 | radius: 48 / 230 * root.radius
73 | samples: 32
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Qml/Joystick.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.0
2 |
3 | Item {
4 | id: root
5 | width: 1340
6 | height: 1340
7 |
8 | property double scaleRatio: 1
9 | property string color: "#22262a"
10 | property bool pressedOnHandle: false
11 | property double axisX: 0
12 | property double axisY: 0
13 | property double theta: 0
14 | property double radius: 0
15 | readonly property double maxRadius: 275 * scaleRatio
16 |
17 | function update() {
18 | handle.x = 440 * scaleRatio + maxRadius * axisX
19 | handle.y = 440 * scaleRatio - maxRadius * axisY
20 |
21 | for (let i = 0; i < lights.lights.count; i++) {
22 | let tempTheta = -theta
23 |
24 | if (tempTheta < 0)
25 | tempTheta += 2 * Math.PI
26 |
27 | let angleBetween = Math.abs(tempTheta - lights.lights.itemAt(i).angle)
28 |
29 | lights.lights.itemAt(i).lightOn = (angleBetween < Math.PI / 6 || angleBetween > 2 * Math.PI - Math.PI / 6)
30 | lights.lights.itemAt(i).lightBrightness = Math.min(axisX * axisX + axisY * axisY, 1)
31 | }
32 | }
33 |
34 | function reset() {
35 | axisX = 0
36 | axisY = 0
37 | theta = 0
38 | pressedOnHandle = false
39 | root.update()
40 | }
41 |
42 | onScaleRatioChanged: reset()
43 |
44 | Item {
45 | id: center
46 | x: root.width / 2
47 | y: root.height / 2
48 | }
49 |
50 | Back {
51 | x: 158 * scaleRatio
52 | y: 158 * scaleRatio
53 | radius: 512 * scaleRatio
54 | color: root.color
55 | }
56 |
57 | Groove {
58 | x: 314 * scaleRatio
59 | y: 314 * scaleRatio
60 | radius: 356 * scaleRatio
61 | color: root.color
62 | }
63 |
64 | Lights {
65 | id: lights
66 | x: 10 * scaleRatio
67 | y: 10 * scaleRatio
68 | radius: 660 * scaleRatio
69 | }
70 |
71 | Handle {
72 | id: handle
73 | x: 440 * scaleRatio
74 | y: 440 * scaleRatio
75 | radius: 230 * scaleRatio
76 | color: root.color
77 |
78 | Item {
79 | id: handleCenter
80 | anchors.centerIn: handle
81 | }
82 | }
83 |
84 | MouseArea {
85 | anchors.fill: parent
86 | onPressed: {
87 | let point = mapToItem(handleCenter, mouseX, mouseY)
88 | pressedOnHandle = point.x * point.x + point.y * point.y < handle.radius * handle.radius
89 | }
90 |
91 | onReleased: reset()
92 |
93 | onPositionChanged: {
94 | if (pressedOnHandle) {
95 | let point = mapToItem(center, mouseX, mouseY)
96 | let length = Math.sqrt(point.x * point.x + point.y * point.y)
97 | if (length > maxRadius)
98 | length = maxRadius
99 |
100 | theta = Math.atan2(-point.y, point.x)
101 |
102 | axisX = length * Math.cos(theta) / maxRadius
103 | axisY = length * Math.sin(theta) / maxRadius
104 |
105 | root.update()
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Qml/Lights.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.0
2 |
3 | Item {
4 | id: root
5 | width: 2 * radius
6 | height: 2 * radius
7 |
8 | property double radius: 660
9 | property alias lights: lights
10 |
11 | Repeater {
12 | id: lights
13 | model: 36
14 | SparseLight {
15 | property double angle: 2 * index * Math.PI / 36
16 | radius: 12 / 660 * root.radius
17 | lightOn: false
18 | lightBrightness: 0
19 | x: root.radius - radius + (root.radius - radius) * Math.cos(angle)
20 | y: root.radius - radius + (root.radius - radius) * Math.sin(angle)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Qml/Main.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.15
2 | import QtQuick.Window 2.15
3 |
4 | Window {
5 | id: window
6 | width: 800
7 | height: 800
8 | visible: true
9 | color: "#22262a"
10 |
11 | Joystick {
12 | anchors.centerIn: parent
13 | width: 1340 * scaleRatio
14 | height: 1340 * scaleRatio
15 | color: window.color
16 | scaleRatio: 0.7 * Math.min(window.height / 1366, window.width / 768)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Qml/SparseLight.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.0
2 | import QtGraphicalEffects 1.0
3 |
4 | Item {
5 | id: root
6 | width: 2 * radius
7 | height: 2 * radius
8 |
9 | property double radius: 12
10 | property string backgroundColor: "#34393f"
11 | property string lightColor: "#8bfcee"
12 | property bool lightOn: false
13 | property double lightBrightness: 1.0
14 |
15 | Canvas {
16 | id: background
17 | width: 2 * radius
18 | height: 2 * radius
19 | antialiasing: true
20 | smooth: true
21 | contextType: '2d'
22 | onPaint: {
23 | if (context) {
24 | context.reset()
25 | context.beginPath()
26 | context.arc(radius, radius, 0.9 * radius, 0, 2 * Math.PI)
27 | context.fillStyle = backgroundColor
28 | context.fill()
29 | }
30 | }
31 | }
32 |
33 | Canvas {
34 | id: light
35 | width: 2 * radius
36 | height: 2 * radius
37 | antialiasing: true
38 | smooth: true
39 | contextType: '2d'
40 | onPaint: {
41 | if (context) {
42 | context.reset()
43 | context.beginPath()
44 | context.arc(radius, radius, 0.8 * radius, 0, 2 * Math.PI)
45 | context.fillStyle = lightColor
46 | context.fill()
47 | }
48 | }
49 |
50 | visible: lightOn
51 | opacity: lightBrightness
52 | layer.enabled: true
53 | layer.effect: Glow {
54 | anchors.fill: root
55 | radius: 2 * root.radius
56 | samples: 24
57 | color: lightColor
58 | source: light
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/QmlJoystick.pro:
--------------------------------------------------------------------------------
1 | QT += quick
2 |
3 | SOURCES += \
4 | Source/Main.cpp
5 |
6 | RESOURCES += QmlJoystick.qrc
7 |
--------------------------------------------------------------------------------
/QmlJoystick.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | Qml/Back.qml
4 | Qml/Groove.qml
5 | Qml/Handle.qml
6 | Qml/Joystick.qml
7 | Qml/Lights.qml
8 | Qml/Main.qml
9 | Qml/SparseLight.qml
10 |
11 |
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # QmlJoystick
2 | Joystick written in `QML`.
3 |
4 | ## Build
5 | 1) Install `Qt 5.x.y`.
6 | 2) Open `QmlJoystick.pro` with `QtCreator` and build & run it.
7 |
8 | ## Video
9 | https://github.com/user-attachments/assets/61f210fe-0a5b-4191-b154-83180739b8d2
10 |
11 | ## Keywords
12 | `QML`,
13 | `Joystick`,
14 | `Control`,
15 | `QML Sample`.
16 |
--------------------------------------------------------------------------------
/Source/Main.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | int main(int argc, char *argv[])
5 | {
6 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
7 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
8 | #endif
9 | QGuiApplication app(argc, argv);
10 |
11 | QQmlApplicationEngine engine;
12 | const QUrl url(QStringLiteral("qrc:/Qml/Main.qml"));
13 | QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
14 | &app, [url](QObject *obj, const QUrl &objUrl) {
15 | if (!obj && url == objUrl)
16 | QCoreApplication::exit(-1);
17 | }, Qt::QueuedConnection);
18 | engine.load(url);
19 |
20 | return app.exec();
21 | }
22 |
--------------------------------------------------------------------------------