feat(flutter): add shared texture through rive panel (#10451) 7359e8b824

* feat(flutter): add shared texture through rive panel

* feat: updates to rive panel

* chore: revert pub path change

* docs: update api level docs

* feat: expose drawOrder

feat: Add fallback AtlasTypes that don't need float color buffers (#10475) 5e6f683b9e
Floating point color buffers are only supported via extensions in GL.
Previously, the feather atlas would just break when this functionality
wasn't present.

This PR adds support for multiple different AtlasTypes that make use of
various GL extensions to render the atlas. As a final resort, if none of
the other extensions are available, it can split coverage up into rgba8
compoments. This mode works on unextended GL at the cost of quality.

Co-authored-by: Gordon <pggordonhayes@gmail.com>
This commit is contained in:
HayesGordon
2025-09-03 17:20:33 +00:00
parent 38f40c8309
commit ce00421de5
22 changed files with 1169 additions and 3 deletions

View File

@ -0,0 +1 @@
export 'centaur_example/game_widget.dart';

View File

@ -0,0 +1,368 @@
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:rive/rive.dart' as rive;
class Arrow {
static const double appleRadius = 50;
static const appleRadiusSquared = appleRadius * appleRadius;
final rive.Artboard artboard;
final rive.Vec2D heading;
rive.Vec2D translation;
double time = 0;
Arrow({
required this.artboard,
required this.translation,
required this.heading,
});
void dispose() => artboard.dispose();
bool get draws => time >= 0.1;
bool get isDead => time > 2;
void advance(double elapsedSeconds, Set<Apple> apples) {
time += elapsedSeconds;
if (!draws) {
// Arrow is still leaving the bow (fire animation is playing on
// centaur).
return;
} else if (isDead) {
return;
}
// You'd likely use a scene tree to ray cast against here, but this is a
// simple example.
for (var apple in apples) {
// const { explodeTrigger, x, y } = appleInstance;
if ((apple.translation - translation).squaredLength() <
appleRadiusSquared) {
apple.damage();
}
}
translation += heading * elapsedSeconds * 3000;
heading.y += elapsedSeconds;
// Normalize heading.
heading.norm();
}
void draw(rive.Renderer renderer) {
if (!draws) {
return;
}
renderer.save();
var arrowTransform = rive.Mat2D.fromTranslation(
translation,
).mul(rive.Mat2D.fromRotation(rive.Mat2D(), atan2(heading.y, heading.x)));
renderer.transform(arrowTransform);
artboard.draw(renderer);
renderer.restore();
}
}
class Apple {
final rive.Artboard artboard;
final rive.StateMachine machine;
final rive.TriggerInput explode;
final rive.AABB bounds;
rive.Vec2D translation;
bool _isDead = false;
bool get isDead => _deadTime > 1;
double _deadTime = 0;
Apple({
required this.artboard,
required this.machine,
required this.explode,
required this.translation,
}) : bounds = artboard.bounds {
var center = bounds.center();
artboard.renderTransform = rive.Mat2D.fromTranslation(translation - center);
}
void damage() {
if (_isDead) {
return;
}
explode.fire();
_isDead = true;
}
void advance(double elapsedSeconds) {
// We don't advance the state machine here as we do all the apples in a
// single batch call.
if (_isDead) {
_deadTime += elapsedSeconds;
}
}
void draw(rive.Renderer renderer) {
renderer.save();
var center = bounds.center();
renderer.translate(translation.x - center.x, translation.y - center.y);
artboard.draw(renderer);
renderer.restore();
}
void dispose() {
artboard.dispose();
machine.dispose();
}
}
class GhostApple {
final Apple apple;
rive.Vec2D translation;
GhostApple(this.apple, this.translation);
void draw(rive.Renderer renderer) {
renderer.save();
var center = apple.bounds.center();
renderer.translate(translation.x - center.x, translation.y - center.y);
apple.artboard.draw(renderer);
renderer.restore();
}
}
base class CentaurGame extends rive.RenderTexturePainter {
final rive.File riveFile;
final rive.Artboard character;
late rive.StateMachine characterMachine;
final rive.Artboard backgroundTile;
final Set<Arrow> _arrows = {};
final Set<Apple> _apples = {};
late rive.Component target;
rive.Component? _characterRoot;
rive.Component? _arrowLocation;
rive.NumberInput? _moveInput;
rive.TriggerInput? _fireInput;
double _characterX = 0;
double _characterDirection = 1;
double move = 0;
double _currentMoveSpeed = 0;
CentaurGame(this.riveFile)
: character = riveFile.artboard('Character')!,
backgroundTile = riveFile.artboard('Background_tile')! {
characterMachine = character.defaultStateMachine()!;
character.frameOrigin = false;
backgroundTile.frameOrigin = false;
target = character.component('Look')!;
_characterRoot = character.component('Character');
_arrowLocation = character.component('ArrowLocation');
_moveInput = characterMachine.number('Move');
_fireInput = characterMachine.trigger('Fire');
}
@override
void dispose() {
character.dispose();
backgroundTile.dispose();
riveFile.dispose();
super.dispose();
}
rive.Vec2D localCursor = rive.Vec2D();
void aimAt(Offset localPosition) {
localCursor = rive.Vec2D.fromOffset(localPosition);
}
void pointerDown(PointerDownEvent event) {
_fireInput?.fire();
var transform = _arrowLocation?.worldTransform ?? rive.Mat2D();
var artboard = riveFile.artboard('Arrow');
if (artboard == null) {
return;
}
artboard.frameOrigin = false;
var arrowInstance = Arrow(
artboard: artboard,
translation:
transform.translation + rive.Vec2D.fromValues(_characterX, 0.0),
heading: transform.xDirection,
);
_arrows.add(arrowInstance);
}
static const int minApples = 30;
static const int maxApples = 100;
final Stopwatch _appleCooloff = Stopwatch()..start();
void spawnApples(int maxSpawnIterations) {
if (_appleCooloff.elapsedMilliseconds < 10) {
return;
}
_appleCooloff.reset();
_appleCooloff.start();
var rand = Random();
var count = rand.nextInt(maxApples - minApples) + minApples;
var bounds = spawnAppleBounds;
var range = bounds.maximum - bounds.minimum;
int spawnCount = 0;
while (_apples.length < count) {
var artboard = riveFile.artboard('Apple');
if (artboard == null) {
return;
}
var stateMachine = artboard.defaultStateMachine();
if (stateMachine == null) {
artboard.dispose();
return;
}
var explode = stateMachine.trigger('Explode');
if (explode == null) {
artboard.dispose();
stateMachine.dispose();
return;
}
var apple = Apple(
artboard: artboard,
machine: stateMachine,
explode: explode,
translation: rive.Vec2D.fromValues(
bounds.minimum.x + range.x * rand.nextDouble(),
bounds.minimum.y + range.y * rand.nextDouble(),
),
// translation: bounds.minimum,
);
_apples.add(apple);
spawnCount++;
if (spawnCount > maxSpawnIterations) {
return;
}
}
}
rive.AABB get sceneBounds {
final bounds = character.bounds;
final characterWidth = bounds.width;
return bounds.inset(-characterWidth * 5, 0);
}
rive.AABB get spawnAppleBounds {
return rive.AABB.fromMinMax(
sceneBounds.minimum - rive.Vec2D.fromValues(0, 3000),
sceneBounds.maximum - rive.Vec2D.fromValues(0, 600),
);
}
@override
bool paint(
rive.RenderTexture texture,
double devicePixelRatio,
Size size,
double elapsedSeconds,
) {
var renderer = texture.renderer;
var viewTransform = rive.Renderer.computeAlignment(
rive.Fit.contain,
Alignment.bottomCenter,
rive.AABB.fromValues(0, 0, size.width, size.height),
sceneBounds,
devicePixelRatio,
);
// Compute cursor in world space.
final inverseViewTransform = rive.Mat2D();
var worldCursor = rive.Vec2D();
if (rive.Mat2D.invert(inverseViewTransform, viewTransform)) {
worldCursor = inverseViewTransform * localCursor;
// Check if we should invert the character's direction by comparing
// the world location of the cursor to the world location of the
// character (need to compensate by character movement, characterX).
_characterDirection = _characterX < worldCursor.x ? 1 : -1;
_characterRoot?.scaleX = _characterDirection;
}
target.worldTransform = rive.Mat2D.fromTranslation(
worldCursor - rive.Vec2D.fromValues(_characterX, 0),
);
const moveSpeed = 100;
var targetMoveSpeed = move * moveSpeed;
_moveInput?.value = move * _characterDirection.sign;
_currentMoveSpeed +=
(targetMoveSpeed - _currentMoveSpeed) * min(1, elapsedSeconds * 10);
_characterX += elapsedSeconds * _currentMoveSpeed;
characterMachine.advanceAndApply(elapsedSeconds);
renderer.save();
renderer.transform(viewTransform);
double backgroundScale = 3;
for (int b = -2; b <= 2; b++) {
renderer.save();
var xform = rive.Mat2D.fromScale(backgroundScale, backgroundScale);
xform[4] = backgroundScale * b * backgroundTile.bounds.width;
renderer.transform(xform);
backgroundTile.draw(renderer);
renderer.restore();
}
renderer.save();
renderer.translate(_characterX, 0);
character.draw(renderer);
renderer.restore();
var deadArrows = <Arrow>{};
for (final arrow in _arrows) {
if (arrow.isDead) {
deadArrows.add(arrow);
arrow.dispose();
continue;
}
arrow.draw(renderer);
arrow.advance(elapsedSeconds, _apples);
}
_arrows.removeAll(deadArrows);
bool batchRender = true;
// Advance apple state machines in one multi-threaded batch.
if (batchRender) {
rive.Rive.batchAdvanceAndRender(
_apples.map((apple) => apple.machine),
elapsedSeconds,
renderer,
);
// ignore: dead_code
} else {
rive.Rive.batchAdvance(
_apples.map((apple) => apple.machine),
elapsedSeconds,
);
}
var deadApples = <Apple>{};
for (final apple in _apples) {
if (apple.isDead) {
deadApples.add(apple);
apple.dispose();
continue;
}
apple.advance(elapsedSeconds);
// ignore: dead_code
if (!batchRender) {
apple.draw(renderer);
}
}
_apples.removeAll(deadApples);
spawnApples(5);
renderer.restore();
return true;
}
@override
Color get background => const Color(0xFF6F8C9B);
}

View File

@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart' as rive;
import 'package:rive_example/advanced/centaur_example/centaur_game.dart';
class CentaurGameWidget extends StatefulWidget {
const CentaurGameWidget({super.key});
@override
State<CentaurGameWidget> createState() => _CentaurGameWidgetState();
}
class _CentaurGameWidgetState extends State<CentaurGameWidget> {
final rive.RenderTexture _renderTexture =
rive.RiveNative.instance.makeRenderTexture();
CentaurGame? _centaurPainter;
@override
void initState() {
super.initState();
load();
}
Future<void> load() async {
var data = await rootBundle.load('assets/centaur_v2.riv');
var bytes = data.buffer.asUint8List();
var file = await rive.File.decode(bytes, riveFactory: rive.Factory.rive);
if (file != null) {
setState(() {
_centaurPainter = CentaurGame(file);
});
}
}
@override
void dispose() {
super.dispose();
_centaurPainter?.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Centaur Game')),
body: ColoredBox(
color: const Color(0xFF507FBA),
child: Center(
child: _centaurPainter == null
? const SizedBox()
: Focus(
focusNode: FocusNode(
canRequestFocus: true,
onKeyEvent: (node, event) {
if (event is KeyRepeatEvent) {
return KeyEventResult.handled;
}
double speed = 0;
if (event is KeyDownEvent) {
speed = 1;
} else if (event is KeyUpEvent) {
speed = -1;
}
if (event.logicalKey == LogicalKeyboardKey.keyA) {
_centaurPainter!.move -= speed;
} else if (event.logicalKey ==
LogicalKeyboardKey.keyD) {
_centaurPainter!.move += speed;
}
return KeyEventResult.handled;
},
)..requestFocus(),
child: MouseRegion(
onHover: (event) => _centaurPainter!.aimAt(
event.localPosition * View.of(context).devicePixelRatio,
),
child: Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: _centaurPainter!.pointerDown,
onPointerMove: (event) => _centaurPainter!.aimAt(
event.localPosition *
View.of(context).devicePixelRatio,
),
child: _renderTexture.widget(
painter: _centaurPainter!,
),
),
),
),
),
),
),
);
}
}