mirror of
https://github.com/rive-app/rive-flutter.git
synced 2025-11-02 04:37:12 +08:00
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:
1
example/lib/advanced/advanced.dart
Normal file
1
example/lib/advanced/advanced.dart
Normal file
@ -0,0 +1 @@
|
||||
export 'centaur_example/game_widget.dart';
|
||||
368
example/lib/advanced/centaur_example/centaur_game.dart
Normal file
368
example/lib/advanced/centaur_example/centaur_game.dart
Normal 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);
|
||||
}
|
||||
96
example/lib/advanced/centaur_example/game_widget.dart
Normal file
96
example/lib/advanced/centaur_example/game_widget.dart
Normal 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!,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user