Joystick updates

- Adds invert x/y to joystick (Thanks @alxgibsn)
- Adds the invert to Flutter and C++ runtimes.
- Updates Cpp core generator to latest Flutter+NNBD.
- Fixes warnings and errors from latest Flutter (Dart SDK really).

Diffs=
a4fb3dc7d Joystick updates (#5261)
diff --git a/.rive_head b/.rive_head
index 38bffa2..9c8e55f 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-8f8e06b9e56bc373e55a7f9b940d0457fe119200
+a4fb3dc7decb50f6965e21ebced098cbdc35883d
diff --git a/dev/core_generator/lib/main.dart b/dev/core_generator/lib/main.dart
index a48945c..1d433d9 100644
--- a/dev/core_generator/lib/main.dart
+++ b/dev/core_generator/lib/main.dart
@@ -9,7 +9,7 @@
   Directory(defsPath).list(recursive: true).listen(
     (entity) {
       if (entity is File && entity.path.toLowerCase().endsWith('.json')) {
-        Definition(entity.path.substring(defsPath.length));
+        Definition.make(entity.path.substring(defsPath.length));
       }
     },
     onDone: Definition.generate,
diff --git a/dev/core_generator/lib/src/definition.dart b/dev/core_generator/lib/src/definition.dart
index 44a479b..596b969 100644
--- a/dev/core_generator/lib/src/definition.dart
+++ b/dev/core_generator/lib/src/definition.dart
@@ -19,20 +19,20 @@
   final String _filename;
   static final _formatter = CppFormatter();
 
-  String _name;
+  String? _name;
   final List<Property> _properties = [];
 
   List<Property> get properties => _properties
       .where((property) => property.isRuntime)
       .toList(growable: false);
 
-  Definition _extensionOf;
-  Key _key;
+  Definition? _extensionOf;
+  Key? _key;
   bool _isAbstract = false;
   bool _editorOnly = false;
   bool _forRuntime = true;
   bool get forRuntime => _forRuntime;
-  factory Definition(String filename) {
+  static Definition? make(String filename) {
     var definition = definitions[filename];
     if (definition != null) {
       return definition;
@@ -40,7 +40,7 @@
 
     var file = File(defsPath + filename);
     var contents = file.readAsStringSync();
-    Map<String, dynamic> definitionData;
+    late Map<String, dynamic> definitionData;
     try {
       dynamic parsedJson = json.decode(contents);
       if (parsedJson is Map<String, dynamic>) {
@@ -54,10 +54,11 @@
         definition = Definition.fromFilename(filename, definitionData);
     return definition;
   }
+
   Definition.fromFilename(this._filename, Map<String, dynamic> data) {
     dynamic extendsFilename = data['extends'];
     if (extendsFilename is String) {
-      _extensionOf = Definition(extendsFilename);
+      _extensionOf = Definition.make(extendsFilename);
     }
     dynamic nameValue = data['name'];
     if (nameValue is String) {
@@ -81,8 +82,8 @@
     if (properties is Map<String, dynamic>) {
       for (final MapEntry<String, dynamic> entry in properties.entries) {
         if (entry.value is Map<String, dynamic>) {
-          var property =
-              Property(this, entry.key, entry.value as Map<String, dynamic>);
+          var property = Property.make(
+              this, entry.key, entry.value as Map<String, dynamic>);
           if (property == null) {
             continue;
           }
@@ -95,7 +96,7 @@
       ? _filename.substring(defsPath.length)
       : _filename;
 
-  String get name => _name;
+  String? get name => _name;
 
   String get localCodeFilename => '${stripExtension(_filename)}_base.hpp';
   String get concreteCodeFilename => 'rive/${stripExtension(_filename)}.hpp';
@@ -112,11 +113,12 @@
     var includes = <String>{
       defineContextExtension
           ? 'rive/core.hpp'
-          : _extensionOf.concreteCodeFilename
+          : _extensionOf!.concreteCodeFilename
     };
     for (final property in properties) {
-      if (property.type.include != null) {
-        includes.add(property.type.include);
+      var include = property.type.include;
+      if (include != null) {
+        includes.add(include);
       }
       includes.add('rive/core/field_types/' +
           property.type.snakeRuntimeCoreName +
@@ -141,7 +143,7 @@
     code.writeln('protected:');
     code.writeln('typedef $superTypeName Super;');
     code.writeln('public:');
-    code.writeln('static const uint16_t typeKey = ${_key.intValue};\n');
+    code.writeln('static const uint16_t typeKey = ${_key!.intValue};\n');
 
     code.write(comment(
         'Helper to quickly determine if a core object extends another '
@@ -163,7 +165,7 @@
     if (properties.isNotEmpty) {
       for (final property in properties) {
         code.writeln('static const uint16_t ${property.name}PropertyKey = '
-            '${property.key.intValue};');
+            '${property.key!.intValue};');
       }
       if (properties.any((prop) => !prop.isEncoded)) {
         code.writeln('private:');
@@ -244,7 +246,7 @@
         }
       }
       if (_extensionOf != null) {
-        code.writeln('${_extensionOf.name}::'
+        code.writeln('${_extensionOf!.name}::'
             'copy(object); ');
       }
       code.writeln('}');
@@ -269,7 +271,7 @@
         code.writeln('}');
       }
       if (_extensionOf != null) {
-        code.writeln('return ${_extensionOf.name}::'
+        code.writeln('return ${_extensionOf!.name}::'
             'deserialize(propertyKey, reader); }');
       } else {
         code.writeln('return false; }');
@@ -306,7 +308,7 @@
       concreteCode.writeln('}');
 
       var formattedCode =
-          await _formatter.formatAndGuard(_name, concreteCode.toString());
+          await _formatter.formatAndGuard(_name!, concreteCode.toString());
       concreteFile.writeAsStringSync(formattedCode, flush: true);
     }
     if (!_isAbstract) {
@@ -341,34 +343,34 @@
     Map<int, Property> properties = {};
     for (final definition in definitions.values) {
       if (definition._key?.intValue != null) {
-        var other = ids[definition._key.intValue];
+        var other = ids[definition._key!.intValue];
         if (other != null) {
           color('Duplicate type ids for $definition and $other.',
               front: Styles.RED);
           runGenerator = false;
         } else {
-          ids[definition._key.intValue] = definition;
+          ids[definition._key!.intValue!] = definition;
         }
       }
       for (final property in definition._properties) {
-        if (property.key.isMissing) {
+        if (property.key!.isMissing) {
           continue;
         }
-        var other = properties[property.key.intValue];
+        var other = properties[property.key!.intValue];
         if (other != null) {
           color(
               '''Duplicate field ids for ${property.definition}.$property '''
               '''and ${other.definition}.$other.''',
               front: Styles.RED);
           runGenerator = false;
-        } else if (property.key.intValue < minPropertyId) {
+        } else if (property.key!.intValue! < minPropertyId) {
           color(
               '${property.definition}.$property: ids less than '
               '$minPropertyId are reserved.',
               front: Styles.RED);
           runGenerator = false;
         } else {
-          properties[property.key.intValue] = property;
+          properties[property.key!.intValue!] = property;
         }
       }
     }
@@ -377,16 +379,14 @@
     int nextFieldId = minPropertyId - 1;
     int nextId = 0;
     for (final definition in definitions.values) {
-      if (definition._key != null &&
-          definition._key.intValue != null &&
-          definition._key.intValue > nextId) {
-        nextId = definition._key.intValue;
+      var intValue = definition._key?.intValue;
+      if (intValue != null && intValue > nextId) {
+        nextId = intValue;
       }
       for (final field in definition._properties) {
-        if (field != null &&
-            field.key.intValue != null &&
-            field.key.intValue > nextFieldId) {
-          nextFieldId = field.key.intValue;
+        var intValue = field.key?.intValue;
+        if (intValue != null && intValue > nextFieldId) {
+          nextFieldId = intValue;
         }
       }
     }
@@ -440,10 +440,10 @@
     for (final definition in runtimeDefinitions) {
       for (final property in definition.properties) {
         usedFieldTypes[property.type] ??= [];
-        usedFieldTypes[property.type].add(property);
+        usedFieldTypes[property.type]!.add(property);
         if (!property.isEncoded) {
           getSetFieldTypes[property.type] ??= [];
-          getSetFieldTypes[property.type].add(property);
+          getSetFieldTypes[property.type]!.add(property);
         }
       }
     }
@@ -453,12 +453,14 @@
               'int propertyKey, ${fieldType.cppName} value){');
       ctxCode.writeln('switch (propertyKey) {');
       var properties = getSetFieldTypes[fieldType];
-      for (final property in properties) {
-        ctxCode.writeln('case ${property.definition.name}Base'
-            '::${property.name}PropertyKey:');
-        ctxCode.writeln('object->as<${property.definition.name}Base>()->'
-            '${property.name}(value);');
-        ctxCode.writeln('break;');
+      if (properties != null) {
+        for (final property in properties) {
+          ctxCode.writeln('case ${property.definition.name}Base'
+              '::${property.name}PropertyKey:');
+          ctxCode.writeln('object->as<${property.definition.name}Base>()->'
+              '${property.name}(value);');
+          ctxCode.writeln('break;');
+        }
       }
       ctxCode.writeln('}}');
     }
@@ -468,11 +470,14 @@
           'Core* object, int propertyKey){');
       ctxCode.writeln('switch (propertyKey) {');
       var properties = getSetFieldTypes[fieldType];
-      for (final property in properties) {
-        ctxCode.writeln('case ${property.definition.name}Base'
-            '::${property.name}PropertyKey:');
-        ctxCode.writeln('return object->as<${property.definition.name}Base>()->'
-            '${property.name}();');
+      if (properties != null) {
+        for (final property in properties) {
+          ctxCode.writeln('case ${property.definition.name}Base'
+              '::${property.name}PropertyKey:');
+          ctxCode
+              .writeln('return object->as<${property.definition.name}Base>()->'
+                  '${property.name}();');
+        }
       }
       ctxCode.writeln('}');
       ctxCode.writeln('return ${fieldType.defaultValue ?? 'nullptr'};');
@@ -484,9 +489,11 @@
 
     for (final fieldType in usedFieldTypes.keys) {
       var properties = usedFieldTypes[fieldType];
-      for (final property in properties) {
-        ctxCode.writeln('case ${property.definition.name}Base'
-            '::${property.name}PropertyKey:');
+      if (properties != null) {
+        for (final property in properties) {
+          ctxCode.writeln('case ${property.definition.name}Base'
+              '::${property.name}PropertyKey:');
+        }
       }
       ctxCode.writeln('return Core${fieldType.capitalizedName}Type::id;');
     }
@@ -496,10 +503,9 @@
     ctxCode.writeln('};}');
 
     var output = generatedHppPath;
-    var folder =
-        output != null && output.isNotEmpty && output[output.length - 1] == '/'
-            ? output.substring(0, output.length - 1)
-            : output;
+    var folder = output.isNotEmpty && output[output.length - 1] == '/'
+        ? output.substring(0, output.length - 1)
+        : output;
 
     var file = File('$folder/core_registry.hpp');
     file.createSync(recursive: true);
diff --git a/dev/core_generator/lib/src/field_type.dart b/dev/core_generator/lib/src/field_type.dart
index 486bd92..60f3025 100644
--- a/dev/core_generator/lib/src/field_type.dart
+++ b/dev/core_generator/lib/src/field_type.dart
@@ -8,26 +8,25 @@
 
 abstract class FieldType {
   final String name;
-  String _cppName;
-  final String include;
-  String get cppName => _cppName;
-  String get cppGetterName => _cppName;
+  String? _cppName;
+  final String? include;
+  String? get cppName => _cppName;
+  String? get cppGetterName => _cppName;
 
-  String _runtimeCoreType;
+  final String _runtimeCoreType;
   String get runtimeCoreType => _runtimeCoreType;
 
   FieldType(
     this.name,
-    String runtimeCoreType, {
-    String cppName,
+    this._runtimeCoreType, {
+    String? cppName,
     this.include,
   }) {
     _cppName = cppName ?? name;
-    _runtimeCoreType = runtimeCoreType;
     _types[name] = this;
   }
 
-  static FieldType find(dynamic key) {
+  static FieldType? find(dynamic key) {
     if (key is! String) {
       return null;
     }
@@ -40,10 +39,10 @@
   }
 
   String equalityCheck(String varAName, String varBName) {
-    return "$varAName == $varBName";
+    return '$varAName == $varBName';
   }
 
-  String get defaultValue => null;
+  String? get defaultValue => null;
 
   String get uncapitalizedName => '${name[0].toLowerCase()}${name.substring(1)}'
       .replaceAll('<', '')
@@ -61,5 +60,5 @@
       .replaceAllMapped(RegExp('(.+?)([A-Z])'), (Match m) => '${m[1]}_${m[2]}')
       .toLowerCase();
 
-  String convertCpp(String value) => value;
+  String? convertCpp(String value) => value;
 }
diff --git a/dev/core_generator/lib/src/field_types/bytes_field_type.dart b/dev/core_generator/lib/src/field_types/bytes_field_type.dart
index 163772b..3ff7f55 100644
--- a/dev/core_generator/lib/src/field_types/bytes_field_type.dart
+++ b/dev/core_generator/lib/src/field_types/bytes_field_type.dart
@@ -16,7 +16,7 @@
   String get cppGetterName => 'Span<const uint8_t>';
 
   @override
-  String convertCpp(String value) {
+  String? convertCpp(String value) {
     return null;
   }
 }
diff --git a/dev/core_generator/lib/src/field_types/double_field_type.dart b/dev/core_generator/lib/src/field_types/double_field_type.dart
index 69e4f49..1f565bb 100644
--- a/dev/core_generator/lib/src/field_types/double_field_type.dart
+++ b/dev/core_generator/lib/src/field_types/double_field_type.dart
@@ -7,7 +7,7 @@
   String get defaultValue => '0.0f';
 
   @override
-  String convertCpp(String value) {
+  String? convertCpp(String value) {
     var result = value;
     if (result.isNotEmpty) {
       if (result[result.length - 1] != 'f') {
diff --git a/dev/core_generator/lib/src/field_types/initialize.dart b/dev/core_generator/lib/src/field_types/initialize.dart
index 01c0b2a..5d37260 100644
--- a/dev/core_generator/lib/src/field_types/initialize.dart
+++ b/dev/core_generator/lib/src/field_types/initialize.dart
@@ -3,7 +3,7 @@
 import 'package:core_generator/src/field_type.dart';
 import 'package:core_generator/src/field_types/bytes_field_type.dart';
 
-List<FieldType> fields;
+late List<FieldType> fields;
 
 void initializeFields() {
   fields = [
diff --git a/dev/core_generator/lib/src/field_types/string_field_type.dart b/dev/core_generator/lib/src/field_types/string_field_type.dart
index db4842f..3fb6d9f 100644
--- a/dev/core_generator/lib/src/field_types/string_field_type.dart
+++ b/dev/core_generator/lib/src/field_types/string_field_type.dart
@@ -11,7 +11,7 @@
   String get cppGetterName => 'const std::string&';
 
   @override
-  String convertCpp(String value) {
+  String? convertCpp(String value) {
     var result = value;
     if (result.length > 1) {
       if (result[0] == '\'') {
diff --git a/dev/core_generator/lib/src/field_types/uint_field_type.dart b/dev/core_generator/lib/src/field_types/uint_field_type.dart
index daa2457..df9b751 100644
--- a/dev/core_generator/lib/src/field_types/uint_field_type.dart
+++ b/dev/core_generator/lib/src/field_types/uint_field_type.dart
@@ -13,5 +13,6 @@
 
   // We do this to fix up CoreContext.invalidProperyKey
   @override
-  String convertCpp(String value) => value.replaceAll('CoreContext.', 'Core::');
+  String? convertCpp(String value) =>
+      value.replaceAll('CoreContext.', 'Core::');
 }
diff --git a/dev/core_generator/lib/src/key.dart b/dev/core_generator/lib/src/key.dart
index a455656..8e1a8d0 100644
--- a/dev/core_generator/lib/src/key.dart
+++ b/dev/core_generator/lib/src/key.dart
@@ -2,14 +2,14 @@
 import 'property.dart';
 
 class Key {
-  final String stringValue;
-  final int intValue;
+  final String? stringValue;
+  final int? intValue;
 
   bool get isMissing => intValue == null;
 
   Key(this.stringValue, this.intValue);
   Key.forDefinition(Definition def)
-      : stringValue = def.name.toLowerCase(),
+      : stringValue = def.name?.toLowerCase(),
         intValue = null;
   Key.forProperty(Property field)
       : stringValue = field.name.toLowerCase(),
@@ -17,7 +17,7 @@
 
   Key withIntValue(int id) => Key(stringValue, id);
 
-  factory Key.fromJSON(dynamic data) {
+  static Key? fromJSON(dynamic data) {
     if (data is! Map<String, dynamic>) {
       return null;
     }
diff --git a/dev/core_generator/lib/src/property.dart b/dev/core_generator/lib/src/property.dart
index 76b3be8..e13ae67 100644
--- a/dev/core_generator/lib/src/property.dart
+++ b/dev/core_generator/lib/src/property.dart
@@ -7,22 +7,23 @@
   final String name;
   final FieldType type;
   final Definition definition;
-  String initialValue;
-  String initialValueRuntime;
+  String? initialValue;
+  String? initialValueRuntime;
   bool isVirtual = false;
   bool animates = false;
-  String group;
-  Key key;
-  String description;
+  String? group;
+  Key? key;
+  String? description;
   bool isNullable = false;
   bool isRuntime = true;
   bool isCoop = true;
   bool isSetOverride = false;
   bool isGetOverride = false;
   bool isEncoded = false;
-  FieldType typeRuntime;
+  FieldType? typeRuntime;
 
-  factory Property(Definition type, String name, Map<String, dynamic> data) {
+  static Property? make(
+      Definition type, String name, Map<String, dynamic> data) {
     if (data['runtime'] is bool && data['runtime'] == false) {
       return null;
     }
@@ -34,11 +35,10 @@
       color('Invalid field type ${data['type']} for $name.', front: Styles.RED);
       return null;
     }
-    return Property.make(type, name, fieldType, data);
+    return Property(type, name, fieldType, data);
   }
 
-  Property.make(
-      this.definition, this.name, this.type, Map<String, dynamic> data) {
+  Property(this.definition, this.name, this.type, Map<String, dynamic> data) {
     dynamic encodedValue = data['encoded'];
     if (encodedValue is bool) {
       isEncoded = encodedValue;
@@ -99,9 +99,7 @@
   FieldType getExportType() => typeRuntime ?? type;
 
   @override
-  String toString() {
-    return '$name(${key.intValue})';
-  }
+  String toString() => '$name(${key?.intValue})';
 
   String get capitalizedName => '${name[0].toUpperCase()}${name.substring(1)}'
       .replaceAll('<', '')
diff --git a/dev/core_generator/pubspec.yaml b/dev/core_generator/pubspec.yaml
index 6e7eb7c..5227dc5 100644
--- a/dev/core_generator/pubspec.yaml
+++ b/dev/core_generator/pubspec.yaml
@@ -1,7 +1,7 @@
 name: core_generator
 
 environment:
-    sdk: ">=2.6.0 <3.0.0"
+  sdk: ">=2.12.0 <3.0.0"
 
 dependencies:
-    colorize: ^2.0.0
+  colorize: ^3.0.0
diff --git a/dev/defs/joystick.json b/dev/defs/joystick.json
index 4ef5e38..17137f0 100644
--- a/dev/defs/joystick.json
+++ b/dev/defs/joystick.json
@@ -32,8 +32,7 @@
       "key": {
         "int": 303,
         "string": "posx"
-      },
-      "runtime": false
+      }
     },
     "posY": {
       "type": "double",
@@ -41,8 +40,7 @@
       "key": {
         "int": 304,
         "string": "posy"
-      },
-      "runtime": false
+      }
     },
     "originX": {
       "type": "double",
@@ -51,8 +49,7 @@
         "int": 307,
         "string": "originx"
       },
-      "description": "Origin x in normalized coordinates (0.5 = center, 0 = left, 1 = right).",
-      "runtime": false
+      "description": "Origin x in normalized coordinates (0.5 = center, 0 = left, 1 = right)."
     },
     "originY": {
       "type": "double",
@@ -61,8 +58,7 @@
         "int": 308,
         "string": "originy"
       },
-      "description": "Origin y in normalized coordinates (0.5 = center, 0 = top, 1 = bottom).",
-      "runtime": false
+      "description": "Origin y in normalized coordinates (0.5 = center, 0 = top, 1 = bottom)."
     },
     "width": {
       "type": "double",
@@ -70,8 +66,7 @@
       "key": {
         "int": 305,
         "string": "width"
-      },
-      "runtime": false
+      }
     },
     "height": {
       "type": "double",
@@ -79,8 +74,7 @@
       "key": {
         "int": 306,
         "string": "height"
-      },
-      "runtime": false
+      }
     },
     "xId": {
       "type": "Id",
@@ -103,6 +97,25 @@
         "string": "y_id"
       },
       "description": "Identifier used to track the animation used for the y axis of the joystick."
+    },
+    "joystickFlags": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 312,
+        "string": "joystickflags"
+      }
+    },
+    "handleSourceId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 313,
+        "string": "handlesourceid"
+      },
+      "description": "Identifier used to track the custom handle source of the joystick."
     }
   }
 }
\ No newline at end of file
diff --git a/include/rive/animation/blend_state_1d.hpp b/include/rive/animation/blend_state_1d.hpp
index ebe394a..8b3092d 100644
--- a/include/rive/animation/blend_state_1d.hpp
+++ b/include/rive/animation/blend_state_1d.hpp
@@ -7,10 +7,7 @@
 class BlendState1D : public BlendState1DBase
 {
 public:
-    //  -1 (4294967295) is our flag value for input not set. It means it wasn't set at edit
-    //  time.
-    const uint32_t noInputSpecified = -1;
-    bool hasValidInputId() const { return inputId() != noInputSpecified; }
+    bool hasValidInputId() const { return inputId() != Core::emptyId; }
 
     StatusCode import(ImportStack& importStack) override;
 
diff --git a/include/rive/artboard.hpp b/include/rive/artboard.hpp
index 39e9034..b53430c 100644
--- a/include/rive/artboard.hpp
+++ b/include/rive/artboard.hpp
@@ -43,6 +43,7 @@
     std::vector<DrawTarget*> m_DrawTargets;
     std::vector<NestedArtboard*> m_NestedArtboards;
     std::vector<Joystick*> m_Joysticks;
+    bool m_JoysticksApplyBeforeUpdate = true;
 
     unsigned int m_DirtDepth = 0;
     std::unique_ptr<RenderPath> m_BackgroundPath;
diff --git a/include/rive/core.hpp b/include/rive/core.hpp
index 0b5567f..b397d49 100644
--- a/include/rive/core.hpp
+++ b/include/rive/core.hpp
@@ -12,6 +12,7 @@
 class Core
 {
 public:
+    const uint32_t emptyId = -1;
     static const int invalidPropertyKey = 0;
     virtual ~Core() {}
     virtual uint16_t coreType() const = 0;
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
index 3dd4042..5ba4c82 100644
--- a/include/rive/generated/core_registry.hpp
+++ b/include/rive/generated/core_registry.hpp
@@ -517,6 +517,12 @@
             case JoystickBase::yIdPropertyKey:
                 object->as<JoystickBase>()->yId(value);
                 break;
+            case JoystickBase::joystickFlagsPropertyKey:
+                object->as<JoystickBase>()->joystickFlags(value);
+                break;
+            case JoystickBase::handleSourceIdPropertyKey:
+                object->as<JoystickBase>()->handleSourceId(value);
+                break;
             case WeightBase::valuesPropertyKey:
                 object->as<WeightBase>()->values(value);
                 break;
@@ -781,6 +787,24 @@
             case JoystickBase::yPropertyKey:
                 object->as<JoystickBase>()->y(value);
                 break;
+            case JoystickBase::posXPropertyKey:
+                object->as<JoystickBase>()->posX(value);
+                break;
+            case JoystickBase::posYPropertyKey:
+                object->as<JoystickBase>()->posY(value);
+                break;
+            case JoystickBase::originXPropertyKey:
+                object->as<JoystickBase>()->originX(value);
+                break;
+            case JoystickBase::originYPropertyKey:
+                object->as<JoystickBase>()->originY(value);
+                break;
+            case JoystickBase::widthPropertyKey:
+                object->as<JoystickBase>()->width(value);
+                break;
+            case JoystickBase::heightPropertyKey:
+                object->as<JoystickBase>()->height(value);
+                break;
             case BoneBase::lengthPropertyKey:
                 object->as<BoneBase>()->length(value);
                 break;
@@ -1059,6 +1083,10 @@
                 return object->as<JoystickBase>()->xId();
             case JoystickBase::yIdPropertyKey:
                 return object->as<JoystickBase>()->yId();
+            case JoystickBase::joystickFlagsPropertyKey:
+                return object->as<JoystickBase>()->joystickFlags();
+            case JoystickBase::handleSourceIdPropertyKey:
+                return object->as<JoystickBase>()->handleSourceId();
             case WeightBase::valuesPropertyKey:
                 return object->as<WeightBase>()->values();
             case WeightBase::indicesPropertyKey:
@@ -1238,6 +1266,18 @@
                 return object->as<JoystickBase>()->x();
             case JoystickBase::yPropertyKey:
                 return object->as<JoystickBase>()->y();
+            case JoystickBase::posXPropertyKey:
+                return object->as<JoystickBase>()->posX();
+            case JoystickBase::posYPropertyKey:
+                return object->as<JoystickBase>()->posY();
+            case JoystickBase::originXPropertyKey:
+                return object->as<JoystickBase>()->originX();
+            case JoystickBase::originYPropertyKey:
+                return object->as<JoystickBase>()->originY();
+            case JoystickBase::widthPropertyKey:
+                return object->as<JoystickBase>()->width();
+            case JoystickBase::heightPropertyKey:
+                return object->as<JoystickBase>()->height();
             case BoneBase::lengthPropertyKey:
                 return object->as<BoneBase>()->length();
             case RootBoneBase::xPropertyKey:
@@ -1408,6 +1448,8 @@
             case ArtboardBase::defaultStateMachineIdPropertyKey:
             case JoystickBase::xIdPropertyKey:
             case JoystickBase::yIdPropertyKey:
+            case JoystickBase::joystickFlagsPropertyKey:
+            case JoystickBase::handleSourceIdPropertyKey:
             case WeightBase::valuesPropertyKey:
             case WeightBase::indicesPropertyKey:
             case TendonBase::boneIdPropertyKey:
@@ -1495,6 +1537,12 @@
             case ArtboardBase::originYPropertyKey:
             case JoystickBase::xPropertyKey:
             case JoystickBase::yPropertyKey:
+            case JoystickBase::posXPropertyKey:
+            case JoystickBase::posYPropertyKey:
+            case JoystickBase::originXPropertyKey:
+            case JoystickBase::originYPropertyKey:
+            case JoystickBase::widthPropertyKey:
+            case JoystickBase::heightPropertyKey:
             case BoneBase::lengthPropertyKey:
             case RootBoneBase::xPropertyKey:
             case RootBoneBase::yPropertyKey:
diff --git a/include/rive/generated/joystick_base.hpp b/include/rive/generated/joystick_base.hpp
index 401188e..d32b1a4 100644
--- a/include/rive/generated/joystick_base.hpp
+++ b/include/rive/generated/joystick_base.hpp
@@ -31,14 +31,30 @@
 
     static const uint16_t xPropertyKey = 299;
     static const uint16_t yPropertyKey = 300;
+    static const uint16_t posXPropertyKey = 303;
+    static const uint16_t posYPropertyKey = 304;
+    static const uint16_t originXPropertyKey = 307;
+    static const uint16_t originYPropertyKey = 308;
+    static const uint16_t widthPropertyKey = 305;
+    static const uint16_t heightPropertyKey = 306;
     static const uint16_t xIdPropertyKey = 301;
     static const uint16_t yIdPropertyKey = 302;
+    static const uint16_t joystickFlagsPropertyKey = 312;
+    static const uint16_t handleSourceIdPropertyKey = 313;
 
 private:
     float m_X = 0.0f;
     float m_Y = 0.0f;
+    float m_PosX = 0.0f;
+    float m_PosY = 0.0f;
+    float m_OriginX = 0.5f;
+    float m_OriginY = 0.5f;
+    float m_Width = 100.0f;
+    float m_Height = 100.0f;
     uint32_t m_XId = -1;
     uint32_t m_YId = -1;
+    uint32_t m_JoystickFlags = 0;
+    uint32_t m_HandleSourceId = -1;
 
 public:
     inline float x() const { return m_X; }
@@ -63,6 +79,72 @@
         yChanged();
     }
 
+    inline float posX() const { return m_PosX; }
+    void posX(float value)
+    {
+        if (m_PosX == value)
+        {
+            return;
+        }
+        m_PosX = value;
+        posXChanged();
+    }
+
+    inline float posY() const { return m_PosY; }
+    void posY(float value)
+    {
+        if (m_PosY == value)
+        {
+            return;
+        }
+        m_PosY = value;
+        posYChanged();
+    }
+
+    inline float originX() const { return m_OriginX; }
+    void originX(float value)
+    {
+        if (m_OriginX == value)
+        {
+            return;
+        }
+        m_OriginX = value;
+        originXChanged();
+    }
+
+    inline float originY() const { return m_OriginY; }
+    void originY(float value)
+    {
+        if (m_OriginY == value)
+        {
+            return;
+        }
+        m_OriginY = value;
+        originYChanged();
+    }
+
+    inline float width() const { return m_Width; }
+    void width(float value)
+    {
+        if (m_Width == value)
+        {
+            return;
+        }
+        m_Width = value;
+        widthChanged();
+    }
+
+    inline float height() const { return m_Height; }
+    void height(float value)
+    {
+        if (m_Height == value)
+        {
+            return;
+        }
+        m_Height = value;
+        heightChanged();
+    }
+
     inline uint32_t xId() const { return m_XId; }
     void xId(uint32_t value)
     {
@@ -85,13 +167,43 @@
         yIdChanged();
     }
 
+    inline uint32_t joystickFlags() const { return m_JoystickFlags; }
+    void joystickFlags(uint32_t value)
+    {
+        if (m_JoystickFlags == value)
+        {
+            return;
+        }
+        m_JoystickFlags = value;
+        joystickFlagsChanged();
+    }
+
+    inline uint32_t handleSourceId() const { return m_HandleSourceId; }
+    void handleSourceId(uint32_t value)
+    {
+        if (m_HandleSourceId == value)
+        {
+            return;
+        }
+        m_HandleSourceId = value;
+        handleSourceIdChanged();
+    }
+
     Core* clone() const override;
     void copy(const JoystickBase& object)
     {
         m_X = object.m_X;
         m_Y = object.m_Y;
+        m_PosX = object.m_PosX;
+        m_PosY = object.m_PosY;
+        m_OriginX = object.m_OriginX;
+        m_OriginY = object.m_OriginY;
+        m_Width = object.m_Width;
+        m_Height = object.m_Height;
         m_XId = object.m_XId;
         m_YId = object.m_YId;
+        m_JoystickFlags = object.m_JoystickFlags;
+        m_HandleSourceId = object.m_HandleSourceId;
         Component::copy(object);
     }
 
@@ -105,12 +217,36 @@
             case yPropertyKey:
                 m_Y = CoreDoubleType::deserialize(reader);
                 return true;
+            case posXPropertyKey:
+                m_PosX = CoreDoubleType::deserialize(reader);
+                return true;
+            case posYPropertyKey:
+                m_PosY = CoreDoubleType::deserialize(reader);
+                return true;
+            case originXPropertyKey:
+                m_OriginX = CoreDoubleType::deserialize(reader);
+                return true;
+            case originYPropertyKey:
+                m_OriginY = CoreDoubleType::deserialize(reader);
+                return true;
+            case widthPropertyKey:
+                m_Width = CoreDoubleType::deserialize(reader);
+                return true;
+            case heightPropertyKey:
+                m_Height = CoreDoubleType::deserialize(reader);
+                return true;
             case xIdPropertyKey:
                 m_XId = CoreUintType::deserialize(reader);
                 return true;
             case yIdPropertyKey:
                 m_YId = CoreUintType::deserialize(reader);
                 return true;
+            case joystickFlagsPropertyKey:
+                m_JoystickFlags = CoreUintType::deserialize(reader);
+                return true;
+            case handleSourceIdPropertyKey:
+                m_HandleSourceId = CoreUintType::deserialize(reader);
+                return true;
         }
         return Component::deserialize(propertyKey, reader);
     }
@@ -118,8 +254,16 @@
 protected:
     virtual void xChanged() {}
     virtual void yChanged() {}
+    virtual void posXChanged() {}
+    virtual void posYChanged() {}
+    virtual void originXChanged() {}
+    virtual void originYChanged() {}
+    virtual void widthChanged() {}
+    virtual void heightChanged() {}
     virtual void xIdChanged() {}
     virtual void yIdChanged() {}
+    virtual void joystickFlagsChanged() {}
+    virtual void handleSourceIdChanged() {}
 };
 } // namespace rive
 
diff --git a/include/rive/joystick.hpp b/include/rive/joystick.hpp
index d076a14..e75556e 100644
--- a/include/rive/joystick.hpp
+++ b/include/rive/joystick.hpp
@@ -1,20 +1,39 @@
 #ifndef _RIVE_JOYSTICK_HPP_
 #define _RIVE_JOYSTICK_HPP_
 #include "rive/generated/joystick_base.hpp"
+#include "rive/joystick_flags.hpp"
+#include "rive/math/mat2d.hpp"
 #include <stdio.h>
+
 namespace rive
 {
 class Artboard;
 class LinearAnimation;
+class TransformComponent;
 class Joystick : public JoystickBase
 {
 public:
+    void update(ComponentDirt value) override;
     void apply(Artboard* artboard) const;
     StatusCode onAddedClean(CoreContext* context) override;
+    StatusCode onAddedDirty(CoreContext* context) override;
+
+    bool isJoystickFlagged(JoystickFlags flag) const
+    {
+        return (((JoystickFlags)joystickFlags()) & flag) == flag;
+    }
+
+    bool canApplyBeforeUpdate() const { return m_handleSource == nullptr; }
+
+protected:
+    void buildDependencies() override;
 
 private:
+    Mat2D m_worldTransform;
+    Mat2D m_inverseWorldTransform;
     LinearAnimation* m_xAnimation = nullptr;
     LinearAnimation* m_yAnimation = nullptr;
+    TransformComponent* m_handleSource = nullptr;
 };
 } // namespace rive
 
diff --git a/include/rive/joystick_flags.hpp b/include/rive/joystick_flags.hpp
new file mode 100644
index 0000000..d3e73cd
--- /dev/null
+++ b/include/rive/joystick_flags.hpp
@@ -0,0 +1,64 @@
+#ifndef _RIVE_JOYSTICK_FLAGS_HPP_
+#define _RIVE_JOYSTICK_FLAGS_HPP_
+namespace rive
+{
+enum class JoystickFlags : unsigned char
+{
+    /// Whether to invert the application of the x axis.
+    invertX = 1 << 0,
+
+    /// Whether to invert the application of the y axis.
+    invertY = 1 << 1,
+
+    /// Whether this Joystick works in world space.
+    worldSpace = 1 << 2
+};
+
+inline constexpr JoystickFlags operator&(JoystickFlags lhs, JoystickFlags rhs)
+{
+    return static_cast<JoystickFlags>(static_cast<std::underlying_type<JoystickFlags>::type>(lhs) &
+                                      static_cast<std::underlying_type<JoystickFlags>::type>(rhs));
+}
+
+inline constexpr JoystickFlags operator^(JoystickFlags lhs, JoystickFlags rhs)
+{
+    return static_cast<JoystickFlags>(static_cast<std::underlying_type<JoystickFlags>::type>(lhs) ^
+                                      static_cast<std::underlying_type<JoystickFlags>::type>(rhs));
+}
+
+inline constexpr JoystickFlags operator|(JoystickFlags lhs, JoystickFlags rhs)
+{
+    return static_cast<JoystickFlags>(static_cast<std::underlying_type<JoystickFlags>::type>(lhs) |
+                                      static_cast<std::underlying_type<JoystickFlags>::type>(rhs));
+}
+
+inline constexpr JoystickFlags operator~(JoystickFlags rhs)
+{
+    return static_cast<JoystickFlags>(~static_cast<std::underlying_type<JoystickFlags>::type>(rhs));
+}
+
+inline JoystickFlags& operator|=(JoystickFlags& lhs, JoystickFlags rhs)
+{
+    lhs = static_cast<JoystickFlags>(static_cast<std::underlying_type<JoystickFlags>::type>(lhs) |
+                                     static_cast<std::underlying_type<JoystickFlags>::type>(rhs));
+
+    return lhs;
+}
+
+inline JoystickFlags& operator&=(JoystickFlags& lhs, JoystickFlags rhs)
+{
+    lhs = static_cast<JoystickFlags>(static_cast<std::underlying_type<JoystickFlags>::type>(lhs) &
+                                     static_cast<std::underlying_type<JoystickFlags>::type>(rhs));
+
+    return lhs;
+}
+
+inline JoystickFlags& operator^=(JoystickFlags& lhs, JoystickFlags rhs)
+{
+    lhs = static_cast<JoystickFlags>(static_cast<std::underlying_type<JoystickFlags>::type>(lhs) ^
+                                     static_cast<std::underlying_type<JoystickFlags>::type>(rhs));
+
+    return lhs;
+}
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/math/aabb.hpp b/include/rive/math/aabb.hpp
index 81c1c6a..5d0ef5d 100644
--- a/include/rive/math/aabb.hpp
+++ b/include/rive/math/aabb.hpp
@@ -90,6 +90,12 @@
     static void join(AABB& out, const AABB& a, const AABB& b);
 
     void expand(const AABB& other) { join(*this, *this, other); }
+
+    Vec2D factorFrom(Vec2D point) const
+    {
+        return Vec2D((point.x - left()) * 2.0f / width() - 1.0f,
+                     (point.y - top()) * 2.0f / height() - 1.0f);
+    }
 };
 
 } // namespace rive
diff --git a/src/artboard.cpp b/src/artboard.cpp
index ed104ee..2c5b233 100644
--- a/src/artboard.cpp
+++ b/src/artboard.cpp
@@ -150,8 +150,15 @@
                 break;
 
             case JoystickBase::typeKey:
-                m_Joysticks.push_back(object->as<Joystick>());
+            {
+                Joystick* joystick = object->as<Joystick>();
+                if (!joystick->canApplyBeforeUpdate())
+                {
+                    m_JoysticksApplyBeforeUpdate = false;
+                }
+                m_Joysticks.push_back(joystick);
                 break;
+            }
         }
     }
 
@@ -465,12 +472,23 @@
 
 bool Artboard::advance(double elapsedSeconds)
 {
-    for (auto joystick : m_Joysticks)
+    if (m_JoysticksApplyBeforeUpdate)
     {
-        joystick->apply(this);
+        for (auto joystick : m_Joysticks)
+        {
+            joystick->apply(this);
+        }
     }
 
     bool didUpdate = updateComponents();
+    if (!m_JoysticksApplyBeforeUpdate)
+    {
+        for (auto joystick : m_Joysticks)
+        {
+            joystick->apply(this);
+        }
+        updateComponents();
+    }
     for (auto nestedArtboard : m_NestedArtboards)
     {
         if (nestedArtboard->advance((float)elapsedSeconds))
diff --git a/src/joystick.cpp b/src/joystick.cpp
index 5a61281..334face 100644
--- a/src/joystick.cpp
+++ b/src/joystick.cpp
@@ -1,23 +1,94 @@
 #include "rive/joystick.hpp"
 #include "rive/artboard.hpp"
+#include "rive/transform_component.hpp"
 
 using namespace rive;
 
+StatusCode Joystick::onAddedDirty(CoreContext* context)
+{
+    StatusCode status = Super::onAddedDirty(context);
+    if (status != StatusCode::Ok)
+    {
+        return status;
+    }
+    if (handleSourceId() != Core::emptyId)
+    {
+        auto coreObject = context->resolve(handleSourceId());
+        if (coreObject == nullptr || !coreObject->is<TransformComponent>())
+        {
+            return StatusCode::MissingObject;
+        }
+        m_handleSource = static_cast<TransformComponent*>(coreObject);
+    }
+
+    return StatusCode::Ok;
+}
+
 StatusCode Joystick::onAddedClean(CoreContext* context)
 {
     m_xAnimation = artboard()->animation(xId());
     m_yAnimation = artboard()->animation(yId());
+
     return StatusCode::Ok;
 }
 
+void Joystick::buildDependencies()
+{
+    // We only need to update if we're in world space (and that is only required
+    // at runtime if we have a custom handle source).
+    if (m_handleSource != nullptr && parent() != nullptr)
+    {
+        parent()->addDependent(this);
+        m_handleSource->addDependent(this);
+    }
+}
+
+void Joystick::update(ComponentDirt value)
+{
+    if (m_handleSource == nullptr)
+    {
+        return;
+    }
+    if (hasDirt(value, ComponentDirt::WorldTransform | ComponentDirt::Transform))
+    {
+        Mat2D world = Mat2D::fromTranslate(posX(), posY());
+        if (parent() != nullptr && parent()->is<WorldTransformComponent>())
+        {
+            world = parent()->as<WorldTransformComponent>()->worldTransform() * world;
+        }
+
+        if (m_worldTransform != world)
+        {
+            m_worldTransform = world;
+            m_inverseWorldTransform = world.invertOrIdentity();
+        }
+
+        auto pos = m_inverseWorldTransform * m_handleSource->worldTranslation();
+
+        auto localBounds = AABB(-width() * originX(),
+                                -height() * originY(),
+                                -width() * originX() + width(),
+                                -height() * originY() + height());
+
+        auto local = localBounds.factorFrom(pos);
+        x(local.x);
+        y(local.y);
+    }
+}
+
 void Joystick::apply(Artboard* artboard) const
 {
     if (m_xAnimation != nullptr)
     {
-        m_xAnimation->apply(artboard, (x() + 1.0f) / 2.0f * m_xAnimation->durationSeconds());
+        m_xAnimation->apply(artboard,
+                            ((isJoystickFlagged(JoystickFlags::invertX) ? -x() : x()) + 1.0f) /
+                                2.0f * m_xAnimation->durationSeconds());
     }
     if (m_yAnimation != nullptr)
     {
-        m_yAnimation->apply(artboard, (y() + 1.0f) / 2.0f * m_yAnimation->durationSeconds());
+
+        m_yAnimation->apply(artboard,
+                            ((isJoystickFlagged(JoystickFlags::invertY) ? -y() : y()) + 1.0f) /
+                                2.0f * m_yAnimation->durationSeconds());
     }
 }