1
0
mirror of https://github.com/flutter/samples.git synced 2026-04-25 08:22:16 +00:00

added Type Jam puzzle app for review (#1554)

* added Type Jam puzzle app for review

* pr round 2 prep

* updated ci scripts for varfont_shader_puzzle

* resolved unused and minor variable naming issues

* rotator tiles row and col are final vars now

* removed unused import and print from production

* made constructors const where needed

* pages_flow export refactored to directly come from that file

* removed old api commented out section from FragmentShaded

* updated pubspec yaml to correct project name

* dart min version updated; removed unnecessary commented out dependencies from pubspec.yaml

* updated pubspec.yaml min flutter version to ensure FragmentShader support

* added/edited comments for explanation, esp on var fonts; removed obsolete comments

* trailing newline added to pubspec.yaml eof
This commit is contained in:
Brian James
2023-01-12 19:40:47 -05:00
committed by GitHub
parent bea2ef6c66
commit 057728c5d2
164 changed files with 8214 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
// Copyright 2023 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export 'wonky_char.dart';
export 'wonky_anim_palette.dart';
export 'rotator_puzzle.dart';
export 'lightboxed_panel.dart';
export 'fragment_shaded.dart';

View File

@@ -0,0 +1,271 @@
// Copyright 2023 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class FragmentShaded extends StatefulWidget {
final Widget child;
final String shaderName;
final int shaderDuration;
static const int dampenDuration = 1000;
static final Map<String, ui.FragmentProgram> fragmentPrograms = {};
static const List<String> fragmentProgramNames = [
'nothing',
'bw_split',
'color_split',
'row_offset',
'wavy_circ',
'wavy',
'wavy2'
];
const FragmentShaded({
required this.shaderName,
required this.shaderDuration,
required this.child,
super.key,
});
@override
State<FragmentShaded> createState() => FragmentShadedState();
}
class FragmentShadedState extends State<FragmentShaded>
with TickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _dampenAnimation;
late final Animation<double> _dampenCurve;
late final AnimationController _dampenController;
late AnimatingSamplerBuilder builder;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: widget.shaderDuration),
)..repeat(reverse: false);
_dampenController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: FragmentShaded.dampenDuration),
);
_dampenCurve = CurvedAnimation(
parent: _dampenController,
curve: Curves.easeInOut,
);
_dampenAnimation =
Tween<double>(begin: 1.0, end: 0.0).animate(_dampenCurve);
initializeFragmentProgramsAndBuilder();
}
void initializeFragmentProgramsAndBuilder() async {
if (FragmentShaded.fragmentPrograms.isEmpty) {
for (String s in FragmentShaded.fragmentProgramNames) {
FragmentShaded.fragmentPrograms[s] =
await ui.FragmentProgram.fromAsset('shaders/$s.frag');
}
}
builder = AnimatingSamplerBuilder(_controller, _dampenAnimation,
FragmentShaded.fragmentPrograms[widget.shaderName]!.fragmentShader());
setState(() {});
}
@override
void dispose() {
_controller.dispose();
_dampenController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (null == FragmentShaded.fragmentPrograms[widget.shaderName]) {
setState(() {});
return const SizedBox(
width: 0,
height: 0,
);
}
return Transform.scale(
scale: 0.5,
child: ShaderSamplerBuilder(
builder,
child: widget.child,
),
);
}
void startDampening() {
_dampenController.forward();
}
}
class AnimatingSamplerBuilder extends SamplerBuilder {
AnimatingSamplerBuilder(
this.animation, this.dampenAnimation, this.fragmentShader) {
animation.addListener(notifyListeners);
dampenAnimation.addListener(notifyListeners);
}
final Animation<double> animation;
final Animation<double> dampenAnimation;
final ui.FragmentShader fragmentShader;
@override
void paint(ui.Image image, Size size, ui.Canvas canvas) {
// animation
fragmentShader.setFloat(0, animation.value);
// width
fragmentShader.setFloat(1, size.width);
// height
fragmentShader.setFloat(2, size.height);
// dampener
fragmentShader.setFloat(3, dampenAnimation.value);
// sampler
fragmentShader.setImageSampler(0, image);
canvas.drawRect(Offset.zero & size, Paint()..shader = fragmentShader);
}
}
abstract class SamplerBuilder extends ChangeNotifier {
void paint(ui.Image image, Size size, ui.Canvas canvas);
}
class ShaderSamplerBuilder extends StatelessWidget {
const ShaderSamplerBuilder(this.builder, {required this.child, super.key});
final SamplerBuilder builder;
final Widget child;
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: _ShaderSamplerImpl(
builder,
child: child,
));
}
}
class _ShaderSamplerImpl extends SingleChildRenderObjectWidget {
const _ShaderSamplerImpl(this.builder, {super.child});
final SamplerBuilder builder;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderShaderSamplerBuilderWidget(
devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
builder: builder,
);
}
@override
void updateRenderObject(
BuildContext context, covariant RenderObject renderObject) {
(renderObject as _RenderShaderSamplerBuilderWidget)
..devicePixelRatio = MediaQuery.of(context).devicePixelRatio
..builder = builder;
}
}
// A render object that conditionally converts its child into a [ui.Image]
// and then paints it in place of the child.
class _RenderShaderSamplerBuilderWidget extends RenderProxyBox {
// Create a new [_RenderSnapshotWidget].
_RenderShaderSamplerBuilderWidget({
required double devicePixelRatio,
required SamplerBuilder builder,
}) : _devicePixelRatio = devicePixelRatio,
_builder = builder;
/// The device pixel ratio used to create the child image.
double get devicePixelRatio => _devicePixelRatio;
double _devicePixelRatio;
set devicePixelRatio(double value) {
if (value == devicePixelRatio) {
return;
}
_devicePixelRatio = value;
if (_childRaster == null) {
return;
} else {
_childRaster?.dispose();
_childRaster = null;
markNeedsPaint();
}
}
/// The painter used to paint the child snapshot or child widgets.
SamplerBuilder get builder => _builder;
SamplerBuilder _builder;
set builder(SamplerBuilder value) {
if (value == builder) {
return;
}
builder.removeListener(markNeedsPaint);
_builder = value;
builder.addListener(markNeedsPaint);
markNeedsPaint();
}
ui.Image? _childRaster;
@override
void attach(PipelineOwner owner) {
builder.addListener(markNeedsPaint);
super.attach(owner);
}
@override
void detach() {
_childRaster?.dispose();
_childRaster = null;
builder.removeListener(markNeedsPaint);
super.detach();
}
@override
void dispose() {
builder.removeListener(markNeedsPaint);
_childRaster?.dispose();
_childRaster = null;
super.dispose();
}
// Paint [child] with this painting context, then convert to a raster and detach all
// children from this layer.
ui.Image? _paintAndDetachToImage() {
final OffsetLayer offsetLayer = OffsetLayer();
final PaintingContext context =
PaintingContext(offsetLayer, Offset.zero & size);
super.paint(context, Offset.zero);
// This ignore is here because this method is protected by the `PaintingContext`. Adding a new
// method that performs the work of `_paintAndDetachToImage` would avoid the need for this, but
// that would conflict with our goals of minimizing painting context.
// ignore: invalid_use_of_protected_member
context.stopRecordingIfNeeded();
final ui.Image image = offsetLayer.toImageSync(Offset.zero & size,
pixelRatio: devicePixelRatio);
offsetLayer.dispose();
return image;
}
@override
void paint(PaintingContext context, Offset offset) {
if (size.isEmpty) {
_childRaster?.dispose();
_childRaster = null;
return;
}
_childRaster?.dispose();
_childRaster = _paintAndDetachToImage();
builder.paint(_childRaster!, size, context.canvas);
}
}

View File

@@ -0,0 +1,141 @@
// Copyright 2023 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../page_content/pages_flow.dart';
import '../styles.dart';
class LightboxedPanel extends StatefulWidget {
final PageConfig pageConfig;
final List<Widget> content;
final double width = 300;
final Function? onDismiss;
final bool fadeOnDismiss;
final int? autoDismissAfter;
final bool buildButton;
final Color lightBoxBgColor;
final Color cardBgColor;
const LightboxedPanel({
Key? key,
required this.pageConfig,
required this.content,
this.onDismiss,
this.fadeOnDismiss = true,
this.autoDismissAfter,
this.buildButton = true,
this.lightBoxBgColor = const Color.fromARGB(200, 255, 255, 255),
this.cardBgColor = Colors.white,
}) : super(key: key);
@override
State<LightboxedPanel> createState() => _LightboxedPanelState();
}
class _LightboxedPanelState extends State<LightboxedPanel> {
bool _fading = false;
bool _show = true;
late int _fadeOutDur = 200;
@override
void initState() {
_fadeOutDur = widget.fadeOnDismiss ? _fadeOutDur : 0;
if (null != widget.autoDismissAfter) {
_fadeOutDur = 0;
Future.delayed(
Duration(milliseconds: widget.autoDismissAfter!),
handleDismiss,
);
}
super.initState();
}
void handleDismiss() {
if (widget.fadeOnDismiss) {
setState(() {
_fading = true;
});
}
Future.delayed(Duration(milliseconds: _fadeOutDur), () {
setState(() {
if (widget.fadeOnDismiss) {
_show = false;
}
if (null != widget.onDismiss) {
widget.onDismiss!();
}
});
});
}
List<Widget> buttonComponents() {
return [
Column(
children: [
const SizedBox(
height: 8,
),
TextButton(
onPressed: handleDismiss,
style: ButtonStyles.style(),
child: Text(
'OK',
style: TextStyles.bodyStyle()
.copyWith(color: Colors.white, height: 1.2),
),
),
],
),
];
}
@override
Widget build(BuildContext context) {
if (_show) {
return AnimatedOpacity(
opacity: _fading ? 0 : 1,
curve: Curves.easeOut,
duration: Duration(milliseconds: _fadeOutDur),
child: DecoratedBox(
decoration: BoxDecoration(color: widget.lightBoxBgColor),
child: Center(
child: SizedBox(
width: widget.width,
child: DecoratedBox(
decoration: BoxDecoration(
color: widget.cardBgColor,
border: Border.all(
color: const Color.fromARGB(255, 200, 200, 200),
width: 1.0,
),
boxShadow: const [
BoxShadow(
color: Color.fromARGB(30, 0, 0, 0),
offset: Offset.zero,
blurRadius: 4.0,
spreadRadius: 2.0),
],
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: widget.content +
(widget.buildButton ? buttonComponents() : []),
),
),
),
),
),
),
);
}
return const SizedBox(
width: 0,
height: 0,
);
}
}

View File

@@ -0,0 +1,428 @@
// Copyright 2023 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'components.dart';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import '../model/puzzle_model.dart';
import '../page_content/pages_flow.dart';
class RotatorPuzzle extends StatefulWidget {
final PageConfig pageConfig;
final int numTiles;
final int puzzleNum;
final String shaderKey;
final int shaderDuration;
final String tileShadedString;
final double tileShadedStringSize;
final EdgeInsets tileShadedStringPadding;
final int tileShadedStringAnimDuration;
final List<WonkyAnimSetting> tileShadedStringAnimSettings;
final double tileScaleModifier;
const RotatorPuzzle({
Key? key,
required this.pageConfig,
required this.numTiles,
required this.puzzleNum,
required this.shaderKey,
required this.shaderDuration,
required this.tileShadedString,
required this.tileShadedStringSize,
required this.tileShadedStringPadding,
required this.tileShadedStringAnimDuration,
this.tileShadedStringAnimSettings = const [],
this.tileScaleModifier = 1.0,
}) : super(key: key);
@override
State<RotatorPuzzle> createState() => RotatorPuzzleState();
}
class RotatorPuzzleState extends State<RotatorPuzzle>
with TickerProviderStateMixin {
late PuzzleModel puzzleModel;
bool solved = false;
late final AnimationController animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
);
late final CurvedAnimation animationCurve = CurvedAnimation(
parent: animationController,
curve: const Interval(
0.2,
0.45,
curve: Curves.easeOut,
),
);
late Animation<double> opacAnimation =
Tween<double>(begin: 0.4, end: 1.0).animate(animationCurve)
..addListener(() {
setState(() {});
});
List<GlobalKey<RotatorPuzzleTileState>> tileKeys = [];
GlobalKey<FragmentShadedState> shadedWidgetStackHackStateKey = GlobalKey();
GlobalKey shadedWidgetRepaintBoundaryKey = GlobalKey();
GlobalKey<WonkyCharState> tileBgWonkyCharKey = GlobalKey();
ui.Image? shadedImg;
@override
void initState() {
for (int i = 0; i < widget.numTiles; i++) {
tileKeys.add(GlobalKey<RotatorPuzzleTileState>());
}
puzzleModel = PuzzleModel(
dim: widget.numTiles,
); //TODO check if correct; correlate dim and numTiles; probably get rid of numTiles
generateTiles();
shuffle();
super.initState();
}
List<RotatorPuzzleTile> generateTiles() {
// TODO move to build?
List<RotatorPuzzleTile> tiles = [];
int dim = sqrt(widget.numTiles).round();
for (int i = 0; i < widget.numTiles; i++) {
RotatorPuzzleTile tile = RotatorPuzzleTile(
key: tileKeys[i],
tileID: i,
row: (i / dim).floor(),
col: i % dim,
parentState: this,
shaderKey: widget.shaderKey,
shaderDuration: widget.shaderDuration,
tileShadedString: widget.tileShadedString,
tileShadedStringSize: widget.tileShadedStringSize,
tileShadedStringPadding: widget.tileShadedStringPadding,
animationSettings: widget.tileShadedStringAnimSettings,
tileShadedStringAnimDuration: widget.tileShadedStringAnimDuration,
tileScaleModifier: widget.tileScaleModifier,
);
tiles.add(tile);
}
return tiles;
}
void handlePointerDown({required int tileID}) {
puzzleModel.rotateTile(tileID);
if (puzzleModel.allRotationsCorrect()) {
handleSolved();
}
}
void handleSolved() {
animationController.addStatusListener((status) {
solved = true;
for (GlobalKey<RotatorPuzzleTileState> k in tileKeys) {
if (null != k.currentState && k.currentState!.mounted) {
startDampening();
tileBgWonkyCharKey.currentState!.stopAnimation();
}
}
if (status == AnimationStatus.completed) {
Future.delayed(
const Duration(milliseconds: FragmentShaded.dampenDuration + 250),
() {
widget.pageConfig.pageController.nextPage(
duration:
const Duration(milliseconds: PagesFlow.pageScrollDuration),
curve: Curves.easeOut,
);
});
}
});
animationController.forward();
}
void shuffle() {
Random rng = Random(0xC00010FF);
for (int i = 0; i < widget.numTiles; i++) {
int rando = rng.nextInt(3);
puzzleModel.setTileStatus(i, rando);
if (puzzleModel.allRotationsCorrect()) {
// fallback to prevent starting on solved puzzle
puzzleModel.setTileStatus(0, 1);
}
}
}
double tileSize() {
return widget.pageConfig.puzzleSize / sqrt(widget.numTiles);
}
List<double> tileCoords({required int row, required int col}) {
return <double>[col * tileSize(), row * tileSize()];
}
void setImageFromRepaintBoundary(GlobalKey which) {
final BuildContext? context = which.currentContext;
if (null != context) {
final RenderRepaintBoundary boundary =
context.findRenderObject()! as RenderRepaintBoundary;
final ui.Image img = boundary.toImageSync();
if (mounted) {
setState(() {
shadedImg = img;
});
}
}
}
void startDampening() {
if (null != shadedWidgetStackHackStateKey.currentState &&
shadedWidgetStackHackStateKey.currentState!.mounted) {
shadedWidgetStackHackStateKey.currentState!.startDampening();
}
}
@override
Widget build(BuildContext context) {
// TODO fix widget implementation to remove the need for this hack
// to force a setState rebuild
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {});
}
});
// end hack ----------------
setImageFromRepaintBoundary(shadedWidgetRepaintBoundaryKey);
return Center(
child: SizedBox(
width: widget.pageConfig.puzzleSize,
height: widget.pageConfig.puzzleSize,
child: Opacity(
opacity: opacAnimation.value,
child: Stack(
children: <Widget>[
Positioned(
left: -9999,
top: -9999,
child: RepaintBoundary(
key: shadedWidgetRepaintBoundaryKey,
child: SizedBox(
width: widget.pageConfig.puzzleSize * 4,
height: widget.pageConfig.puzzleSize * 4,
child: Center(
child: FragmentShaded(
key: shadedWidgetStackHackStateKey,
shaderName: widget.shaderKey,
shaderDuration: widget.shaderDuration,
child: Padding(
padding: widget.tileShadedStringPadding,
child: WonkyChar(
key: tileBgWonkyCharKey,
text: widget.tileShadedString,
size: widget.tileShadedStringSize,
animDurationMillis:
widget.tileShadedStringAnimDuration,
animationSettings:
widget.tileShadedStringAnimSettings,
),
),
),
),
),
),
),
] +
generateTiles(),
),
),
),
);
}
}
////////////////////////////////////////////////////////
class RotatorPuzzleTile extends StatefulWidget {
final int tileID;
final RotatorPuzzleState parentState;
final String shaderKey;
final int shaderDuration;
final String tileShadedString;
final double tileShadedStringSize;
final EdgeInsets tileShadedStringPadding;
final int tileShadedStringAnimDuration;
final List<WonkyAnimSetting> animationSettings;
final double tileScaleModifier;
// TODO get row/col out into model
final int row;
final int col;
RotatorPuzzleTile({
Key? key,
required this.tileID,
required this.row,
required this.col,
required this.parentState,
required this.shaderKey,
required this.shaderDuration,
required this.tileShadedString,
required this.tileShadedStringSize,
required this.tileShadedStringPadding,
required this.animationSettings,
required this.tileShadedStringAnimDuration,
required this.tileScaleModifier,
}) : super(key: key);
final State<RotatorPuzzleTile> tileState = RotatorPuzzleTileState();
@override
State<RotatorPuzzleTile> createState() => RotatorPuzzleTileState();
}
class RotatorPuzzleTileState extends State<RotatorPuzzleTile>
with TickerProviderStateMixin {
double touchedOpac = 0.0;
Duration touchedOpacDur = const Duration(milliseconds: 50);
late final AnimationController animationController = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 100,
),
);
late final CurvedAnimation animationCurve = CurvedAnimation(
parent: animationController,
curve: Curves.easeOut,
);
late Animation<double> animation;
@override
void initState() {
super.initState();
animation = Tween<double>(
// initialize animation to starting point
begin: currentStatus() * pi * 0.5,
end: currentStatus() * pi * 0.5,
).animate(animationController);
}
@override
Widget build(BuildContext context) {
// TODO fix widget implementation to remove the need for this hack
// to force a setState rebuild
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {});
}
});
// end hack ------------------------------
List<double> coords =
widget.parentState.tileCoords(row: widget.row, col: widget.col);
double zeroPoint = widget.parentState.widget.pageConfig.puzzleSize * .5 -
widget.parentState.tileSize() * 0.5;
return Stack(
children: [
Stack(
children: [
Positioned(
left: coords[0],
top: coords[1],
child: Transform(
transform: Matrix4.rotationZ(animation.value),
alignment: Alignment.center,
child: GestureDetector(
onTap: handlePointerDown,
child: ClipRect(
child: SizedBox(
width: widget.parentState.tileSize(),
height: widget.parentState.tileSize(),
child: OverflowBox(
maxHeight:
widget.parentState.widget.pageConfig.puzzleSize,
maxWidth:
widget.parentState.widget.pageConfig.puzzleSize,
child: Transform.translate(
offset: Offset(
zeroPoint -
widget.col * widget.parentState.tileSize(),
zeroPoint -
widget.row * widget.parentState.tileSize(),
),
child: SizedBox(
width:
widget.parentState.widget.pageConfig.puzzleSize,
height:
widget.parentState.widget.pageConfig.puzzleSize,
child: Transform.scale(
scale: widget.tileScaleModifier,
child: RawImage(
image: widget.parentState.shadedImg,
),
),
),
),
),
),
),
),
),
),
// puzzle tile overlay fades in/out on tap, to indicate touched tile
Positioned(
left: coords[0],
top: coords[1],
child: IgnorePointer(
child: AnimatedOpacity(
opacity: touchedOpac,
duration: touchedOpacDur,
onEnd: () {
if (touchedOpac == 1.0) {
touchedOpac = 0.0;
touchedOpacDur = const Duration(milliseconds: 300);
setState(() {});
}
},
child: DecoratedBox(
decoration: const BoxDecoration(
color: Color.fromARGB(120, 0, 0, 0)),
child: SizedBox(
width: widget.parentState.tileSize(),
height: widget.parentState.tileSize(),
),
),
),
),
),
],
),
],
);
}
void handlePointerDown() {
if (!widget.parentState.solved) {
int oldStatus = currentStatus();
widget.parentState.handlePointerDown(tileID: widget.tileID);
touchedOpac = 1.0;
touchedOpacDur = const Duration(milliseconds: 100);
rotateTile(oldStatus: oldStatus);
setState(() {});
}
}
int currentStatus() {
return widget.parentState.puzzleModel.getTileStatus(widget.tileID);
}
void rotateTile({required int oldStatus}) {
animation = Tween<double>(
begin: oldStatus * pi * 0.5,
end: currentStatus() * pi * 0.5,
).animate(animationController)
..addListener(() {
setState(() {});
});
animationController.reset();
animationController.forward();
}
}

View File

@@ -0,0 +1,334 @@
// Copyright 2023 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:flutter/material.dart';
import '../components/components.dart';
// WonkyAnimPalette class is meant to be used with WonkyChar
// to create animations based on variable font settings (aka 'axes'),
// and a few basic settings like scale, rotation, etc.
// The choice of variable font axes to implement in this class and
// default min/max values for variable font axes are hard-coded
// for Amstelvar font, packaged and used in this project.
// Other variable fonts will have different available axes and min/max values.
//
// See articles on variable fonts at https://fonts.google.com/knowledge/topics/variable_fonts
// See a list of variable fonts in the Google Fonts lineup, along with
// an enumeration of variable font axes at https://fonts.google.com/variablefonts
class WonkyAnimPalette {
const WonkyAnimPalette({
Key? key,
});
static const Curve defaultCurve = Curves.easeInOut;
// basic (settings unrelated to variable font)
static WonkyAnimSetting scale({
double from = 1,
double to = 2,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'basic',
property: 'scale',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting offsetX({
double from = -50,
double to = 50,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'basic',
property: 'offsetX',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting offsetY({
double from = -50,
double to = 50,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'basic',
property: 'offsetY',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting rotation({
double from = -pi,
double to = pi,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'basic',
property: 'rotation',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting color({
Color from = Colors.blue,
Color to = Colors.red,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'basic',
property: 'color',
fromTo: RangeColor(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
// font variants (variable font settings)
static WonkyAnimSetting opticalSize({
double from = 8,
double to = 144,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'fv',
property: 'opsz',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting weight({
double from = 100,
double to = 1000,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'fv',
property: 'wght',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting grade({
double from = -300,
double to = 500,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'fv',
property: 'GRAD',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting slant({
double from = -10,
double to = 0,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'fv',
property: 'slnt',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting width({
double from = 50,
double to = 125,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'fv',
property: 'wdth',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting thickStroke({
double from = 18,
double to = 263,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'fv',
property: 'XOPQ',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting thinStroke({
double from = 15,
double to = 132,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'fv',
property: 'YOPQ',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting counterWd({
double from = 324,
double to = 640,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'fv',
property: 'XTRA',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting upperCaseHt({
double from = 500,
double to = 1000,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'fv',
property: 'YTUC',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting lowerCaseHt({
double from = 420,
double to = 570,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'fv',
property: 'YTLC',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting ascenderHt({
double from = 500,
double to = 983,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'fv',
property: 'YTAS',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting descenderDepth({
double from = -500,
double to = -138,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'fv',
property: 'YTDE',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
static WonkyAnimSetting figureHt({
double from = 425,
double to = 1000,
double startAt = 0,
double endAt = 1,
Curve curve = defaultCurve,
}) {
return WonkyAnimSetting(
type: 'fv',
property: 'YTFI',
fromTo: RangeDbl(from: from, to: to),
startAt: startAt,
endAt: endAt,
curve: curve,
);
}
}

View File

@@ -0,0 +1,225 @@
// Copyright 2023 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'dart:ui';
import 'package:flutter/foundation.dart' show kDebugMode;
class WonkyChar extends StatefulWidget {
final String text;
final double size;
final double baseRotation;
final int animDurationMillis;
final List<WonkyAnimSetting> animationSettings;
const WonkyChar({
Key? key,
required this.text,
required this.size,
this.baseRotation = 0,
this.animDurationMillis = 1000,
this.animationSettings = const <WonkyAnimSetting>[],
}) : super(key: key);
@override
State<WonkyChar> createState() => WonkyCharState();
}
class WonkyCharState extends State<WonkyChar>
with SingleTickerProviderStateMixin {
bool loopingAnimation = true;
late AnimationController _animController;
final List<Animation<double>> _curves = [];
late final List<Animation> _fvAnimations = [];
final List<String> _fvAxes = [];
// default curve and animations in case user sets nothing for them
late final defaultCurve = CurvedAnimation(
parent: _animController,
curve: const Interval(0, 1, curve: Curves.linear));
late Animation _scaleAnimation =
Tween<double>(begin: 1, end: 1).animate(defaultCurve);
late Animation _offsetXAnimation =
Tween<double>(begin: 0, end: 0).animate(defaultCurve);
late Animation _offsetYAnimation =
Tween<double>(begin: 0, end: 0).animate(defaultCurve);
late Animation _rotationAnimation =
Tween<double>(begin: 0, end: 0).animate(defaultCurve);
late Animation _colorAnimation =
ColorTween(begin: Colors.black, end: Colors.black).animate(defaultCurve);
@override
void initState() {
super.initState();
initAnimations(widget.animationSettings);
_animController
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed && loopingAnimation) {
_animController.reverse();
} else if (status == AnimationStatus.dismissed && loopingAnimation) {
_animController.forward();
}
});
_animController.forward();
}
@override
void dispose() {
_animController.dispose();
super.dispose();
}
void stopAnimation() {
_animController.stop();
}
@override
Widget build(BuildContext context) {
List<FontVariation> fontVariations = [];
for (int i = 0; i < _fvAxes.length; i++) {
fontVariations.add(FontVariation(_fvAxes[i], _fvAnimations[i].value));
}
return Transform(
alignment: Alignment.center,
transform: Matrix4.translationValues(
_offsetXAnimation.value, _offsetYAnimation.value, 0)
..scale(_scaleAnimation.value)
..rotateZ(widget.baseRotation + _rotationAnimation.value),
child: IgnorePointer(
child: Text(
widget.text,
textAlign: TextAlign.center,
style: TextStyle(
color: _colorAnimation.value,
fontFamily: 'Amstelvar',
fontSize: widget.size,
fontVariations: fontVariations,
),
),
),
);
}
void initAnimations(List<WonkyAnimSetting> settings) {
_animController = AnimationController(
vsync: this,
duration: Duration(milliseconds: widget.animDurationMillis),
);
for (WonkyAnimSetting s in settings) {
final curve = CurvedAnimation(
parent: _animController,
curve: Interval(s.startAt, s.endAt, curve: s.curve),
);
late Animation animation;
if (s.property == 'color') {
animation =
ColorTween(begin: s.fromTo.fromValue(), end: s.fromTo.toValue())
.animate(curve);
} else {
animation =
Tween<double>(begin: s.fromTo.fromValue(), end: s.fromTo.toValue())
.animate(curve);
}
if (s.type == 'fv') {
_fvAxes.add(s.property);
_fvAnimations.add(animation);
} else if (s.type == 'basic') {
switch (s.property) {
case 'scale':
{
_scaleAnimation = animation;
}
break;
case 'rotation':
{
_rotationAnimation = animation;
}
break;
case 'offsetX':
{
_offsetXAnimation = animation;
}
break;
case 'offsetY':
{
_offsetYAnimation = animation;
}
break;
case 'color':
{
_colorAnimation = animation;
}
break;
default:
{
if (kDebugMode) {
print(
'**ERROR** unrecognized property to animate: ${s.property}');
}
}
break;
}
}
// save refs to all curves just to persist in mem, don't need to touch them again
_curves.add(curve);
}
}
}
abstract class WCRange {
WCRange();
fromValue() {}
toValue() {}
}
class RangeColor implements WCRange {
Color from;
Color to;
RangeColor({required this.from, required this.to});
@override
Color fromValue() {
return from;
}
@override
Color toValue() {
return to;
}
}
class RangeDbl implements WCRange {
double from;
double to;
RangeDbl({required this.from, required this.to});
@override
double fromValue() {
return from;
}
@override
double toValue() {
return to;
}
}
class WonkyAnimSetting {
// just the animation
String type; // 'fv' for fontVariation, 'basic' for everything else
String property; //font variation axis, or 'size'/'rotation'/etc.
WCRange fromTo;
double startAt; // 0 to 1 rel to controller
double endAt; // same as start
Curve curve;
WonkyAnimSetting({
required this.type,
required this.property,
required this.fromTo,
required this.startAt,
required this.endAt,
required this.curve,
});
}