mirror of
https://github.com/flutter/samples.git
synced 2025-11-10 23:08:59 +00:00
Add flutter_web samples (#75)
This commit is contained in:
committed by
Andrew Brogdon
parent
42f2dce01b
commit
3fe927cb29
4
web/slide_puzzle/README.md
Normal file
4
web/slide_puzzle/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Get the numbers in order by clicking tiles next to the open space.
|
||||
|
||||
Created by [Kevin Moore](https://twitter.com/kevmoo). Original source at
|
||||
[github.com/kevmoo/slide_puzzle](https://github.com/kevmoo/slide_puzzle).
|
||||
93
web/slide_puzzle/analysis_options.yaml
Normal file
93
web/slide_puzzle/analysis_options.yaml
Normal file
@@ -0,0 +1,93 @@
|
||||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
||||
linter:
|
||||
rules:
|
||||
- always_declare_return_types
|
||||
- annotate_overrides
|
||||
- avoid_bool_literals_in_conditional_expressions
|
||||
- avoid_classes_with_only_static_members
|
||||
- avoid_empty_else
|
||||
- avoid_function_literals_in_foreach_calls
|
||||
- avoid_init_to_null
|
||||
- avoid_null_checks_in_equality_operators
|
||||
- avoid_relative_lib_imports
|
||||
- avoid_renaming_method_parameters
|
||||
- avoid_return_types_on_setters
|
||||
- avoid_returning_null
|
||||
- avoid_returning_null_for_future
|
||||
- avoid_returning_null_for_void
|
||||
- avoid_returning_this
|
||||
- avoid_shadowing_type_parameters
|
||||
- avoid_single_cascade_in_expression_statements
|
||||
- avoid_types_as_parameter_names
|
||||
- avoid_unused_constructor_parameters
|
||||
- await_only_futures
|
||||
- camel_case_types
|
||||
- cancel_subscriptions
|
||||
- cascade_invocations
|
||||
- comment_references
|
||||
- constant_identifier_names
|
||||
- control_flow_in_finally
|
||||
- directives_ordering
|
||||
- empty_catches
|
||||
- empty_constructor_bodies
|
||||
- empty_statements
|
||||
- file_names
|
||||
- hash_and_equals
|
||||
- implementation_imports
|
||||
- invariant_booleans
|
||||
- iterable_contains_unrelated_type
|
||||
- join_return_with_assignment
|
||||
- library_names
|
||||
- library_prefixes
|
||||
- list_remove_unrelated_type
|
||||
- literal_only_boolean_expressions
|
||||
- no_adjacent_strings_in_list
|
||||
- no_duplicate_case_values
|
||||
- non_constant_identifier_names
|
||||
- null_closures
|
||||
- omit_local_variable_types
|
||||
- only_throw_errors
|
||||
- overridden_fields
|
||||
- package_api_docs
|
||||
- package_names
|
||||
- package_prefixed_library_names
|
||||
- prefer_adjacent_string_concatenation
|
||||
- prefer_collection_literals
|
||||
- prefer_conditional_assignment
|
||||
- prefer_const_constructors
|
||||
- prefer_contains
|
||||
- prefer_equal_for_default_values
|
||||
- prefer_final_fields
|
||||
- prefer_final_locals
|
||||
- prefer_generic_function_type_aliases
|
||||
- prefer_initializing_formals
|
||||
- prefer_interpolation_to_compose_strings
|
||||
- prefer_is_empty
|
||||
- prefer_is_not_empty
|
||||
- prefer_null_aware_operators
|
||||
- prefer_single_quotes
|
||||
- prefer_typing_uninitialized_variables
|
||||
- recursive_getters
|
||||
- slash_for_doc_comments
|
||||
- test_types_in_equals
|
||||
- throw_in_finally
|
||||
- type_init_formals
|
||||
- unawaited_futures
|
||||
- unnecessary_await_in_return
|
||||
- unnecessary_brace_in_string_interps
|
||||
- unnecessary_const
|
||||
- unnecessary_getters_setters
|
||||
- unnecessary_lambdas
|
||||
- unnecessary_new
|
||||
- unnecessary_null_aware_assignments
|
||||
- unnecessary_parenthesis
|
||||
- unnecessary_statements
|
||||
- unnecessary_this
|
||||
- unrelated_type_equality_checks
|
||||
- use_function_type_syntax_for_parameters
|
||||
- use_rethrow_when_possible
|
||||
- valid_regexps
|
||||
- void_checks
|
||||
33
web/slide_puzzle/lib/main.dart
Normal file
33
web/slide_puzzle/lib/main.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'src/core/puzzle_animator.dart';
|
||||
import 'src/flutter.dart';
|
||||
import 'src/puzzle_home_state.dart';
|
||||
|
||||
void main() => runApp(PuzzleApp());
|
||||
|
||||
class PuzzleApp extends StatelessWidget {
|
||||
final int rows, columns;
|
||||
|
||||
PuzzleApp({int columns = 4, int rows = 4})
|
||||
: columns = columns ?? 4,
|
||||
rows = rows ?? 4;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
title: 'Slide Puzzle',
|
||||
home: _PuzzleHome(rows, columns),
|
||||
);
|
||||
}
|
||||
|
||||
class _PuzzleHome extends StatefulWidget {
|
||||
final int _rows, _columns;
|
||||
|
||||
const _PuzzleHome(this._rows, this._columns, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
PuzzleHomeState createState() =>
|
||||
PuzzleHomeState(PuzzleAnimator(_columns, _rows));
|
||||
}
|
||||
31
web/slide_puzzle/lib/src/app_state.dart
Normal file
31
web/slide_puzzle/lib/src/app_state.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'core/puzzle_animator.dart';
|
||||
import 'flutter.dart';
|
||||
import 'shared_theme.dart';
|
||||
|
||||
abstract class AppState {
|
||||
TabController get tabController;
|
||||
|
||||
PuzzleProxy get puzzle;
|
||||
|
||||
bool get autoPlay;
|
||||
|
||||
void setAutoPlay(bool newValue);
|
||||
|
||||
AnimationNotifier get animationNotifier;
|
||||
|
||||
Iterable<SharedTheme> get themeData;
|
||||
|
||||
SharedTheme get currentTheme;
|
||||
|
||||
set currentTheme(SharedTheme theme);
|
||||
}
|
||||
|
||||
abstract class AnimationNotifier implements Listenable {
|
||||
void animate();
|
||||
|
||||
void dispose();
|
||||
}
|
||||
113
web/slide_puzzle/lib/src/core/body.dart
Normal file
113
web/slide_puzzle/lib/src/core/body.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright 2019 The Chromium Authors. 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' show Point;
|
||||
|
||||
const zeroPoint = Point<double>(0, 0);
|
||||
|
||||
const _epsilon = 0.0001;
|
||||
|
||||
/// Represents a point object with a location and velocity.
|
||||
class Body {
|
||||
Point<double> _velocity;
|
||||
Point<double> _location;
|
||||
|
||||
Point<double> get velocity => _velocity;
|
||||
|
||||
Point<double> get location => _location;
|
||||
|
||||
Body({Point<double> location = zeroPoint, Point<double> velocity = zeroPoint})
|
||||
: assert(location.magnitude.isFinite),
|
||||
_location = location,
|
||||
assert(velocity.magnitude.isFinite),
|
||||
_velocity = velocity;
|
||||
|
||||
factory Body.raw(double x, double y, double vx, double vy) =>
|
||||
Body(location: Point(x, y), velocity: Point(vx, vy));
|
||||
|
||||
Body clone() => Body(location: _location, velocity: _velocity);
|
||||
|
||||
/// Add the velocity specified in [delta] to `this`.
|
||||
void kick(Point<double> delta) {
|
||||
assert(delta.magnitude.isFinite);
|
||||
_velocity = delta;
|
||||
}
|
||||
|
||||
/// [drag] must be greater than or equal to zero. It defines the percent of
|
||||
/// the previous velocity that is lost every second.
|
||||
bool animate(double seconds,
|
||||
{Point<double> force = zeroPoint,
|
||||
double drag = 0,
|
||||
double maxVelocity,
|
||||
Point<double> snapTo}) {
|
||||
assert(seconds.isFinite && seconds > 0,
|
||||
'milliseconds must be finite and > 0 (was $seconds)');
|
||||
|
||||
force ??= zeroPoint;
|
||||
assert(force.x.isFinite && force.y.isFinite, 'force must be finite');
|
||||
|
||||
drag ??= 0;
|
||||
assert(drag.isFinite && drag >= 0, 'drag must be finiate and >= 0');
|
||||
|
||||
maxVelocity ??= double.infinity;
|
||||
assert(maxVelocity > 0, 'maxVelocity must be null or > 0');
|
||||
|
||||
final dragVelocity = _velocity * (1 - drag * seconds);
|
||||
|
||||
if (_sameDirection(_velocity, dragVelocity)) {
|
||||
assert(dragVelocity.magnitude <= _velocity.magnitude,
|
||||
'Huh? $dragVelocity $_velocity');
|
||||
_velocity = dragVelocity;
|
||||
} else {
|
||||
_velocity = zeroPoint;
|
||||
}
|
||||
|
||||
// apply force to velocity
|
||||
_velocity += force * seconds;
|
||||
|
||||
// apply terminal velocity
|
||||
if (_velocity.magnitude > maxVelocity) {
|
||||
_velocity = _unitPoint(_velocity) * maxVelocity;
|
||||
}
|
||||
|
||||
// update location
|
||||
final locationDelta = _velocity * seconds;
|
||||
if (locationDelta.magnitude > _epsilon ||
|
||||
(force.magnitude * seconds) > _epsilon) {
|
||||
_location += locationDelta;
|
||||
return true;
|
||||
} else {
|
||||
if (snapTo != null && (_location.distanceTo(snapTo) < _epsilon * 2)) {
|
||||
_location = snapTo;
|
||||
}
|
||||
_velocity = zeroPoint;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'Body @(${_location.x},${_location.y}) ↕(${_velocity.x},${_velocity.y})';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is Body &&
|
||||
other._location == _location &&
|
||||
other._velocity == _velocity;
|
||||
|
||||
// Since this is a mutable class, a constant value is returned for `hashCode`
|
||||
// This ensures values don't get lost in a Hashing data structure.
|
||||
// Note: this means you shouldn't use this type in most Map/Set impls.
|
||||
@override
|
||||
int get hashCode => 199;
|
||||
}
|
||||
|
||||
Point<double> _unitPoint(Point<double> source) {
|
||||
final result = source * (1 / source.magnitude);
|
||||
return Point(result.x.isNaN ? 0 : result.x, result.y.isNaN ? 0 : result.y);
|
||||
}
|
||||
|
||||
bool _sameDirection(Point a, Point b) {
|
||||
return a.x.sign == b.x.sign && a.y.sign == b.y.sign;
|
||||
}
|
||||
12
web/slide_puzzle/lib/src/core/point_int.dart
Normal file
12
web/slide_puzzle/lib/src/core/point_int.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright 2019 The Chromium Authors. 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' as math;
|
||||
|
||||
class Point extends math.Point<int> {
|
||||
Point(int x, int y) : super(x, y);
|
||||
|
||||
@override
|
||||
Point operator +(math.Point<int> other) => Point(x + other.x, y + other.y);
|
||||
}
|
||||
275
web/slide_puzzle/lib/src/core/puzzle.dart
Normal file
275
web/slide_puzzle/lib/src/core/puzzle.dart
Normal file
@@ -0,0 +1,275 @@
|
||||
// Copyright 2019 The Chromium Authors. 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:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' show Random, max;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'point_int.dart';
|
||||
import 'util.dart';
|
||||
|
||||
part 'puzzle_simple.dart';
|
||||
|
||||
part 'puzzle_smart.dart';
|
||||
|
||||
final _rnd = Random();
|
||||
|
||||
final _spacesRegexp = RegExp(' +');
|
||||
|
||||
abstract class Puzzle {
|
||||
int get width;
|
||||
|
||||
int get length;
|
||||
|
||||
int operator [](int index);
|
||||
|
||||
int indexOf(int value);
|
||||
|
||||
List<int> get _intView;
|
||||
|
||||
List<int> _copyData();
|
||||
|
||||
Puzzle _newWithValues(List<int> values);
|
||||
|
||||
Puzzle clone();
|
||||
|
||||
int get height => length ~/ width;
|
||||
|
||||
Puzzle._();
|
||||
|
||||
factory Puzzle._raw(int width, List<int> source) {
|
||||
if (source.length <= 16) {
|
||||
return _PuzzleSmart(width, source);
|
||||
}
|
||||
return _PuzzleSimple(width, source);
|
||||
}
|
||||
|
||||
factory Puzzle.raw(int width, List<int> source) {
|
||||
requireArgument(width >= 3, 'width', 'Must be at least 3.');
|
||||
requireArgument(source.length >= 6, 'source', 'Must be at least 6 items');
|
||||
_validate(source);
|
||||
|
||||
return Puzzle._raw(width, source);
|
||||
}
|
||||
|
||||
factory Puzzle(int width, int height) =>
|
||||
Puzzle.raw(width, _randomList(width, height));
|
||||
|
||||
factory Puzzle.parse(String input) {
|
||||
final rows = LineSplitter.split(input).map((line) {
|
||||
final splits = line.trim().split(_spacesRegexp);
|
||||
return splits.map(int.parse).toList();
|
||||
}).toList();
|
||||
|
||||
return Puzzle.raw(rows.first.length, rows.expand((row) => row).toList());
|
||||
}
|
||||
|
||||
int valueAt(int x, int y) {
|
||||
final i = _getIndex(x, y);
|
||||
return this[i];
|
||||
}
|
||||
|
||||
int get tileCount => length - 1;
|
||||
|
||||
bool isCorrectPosition(int cellValue) => cellValue == this[cellValue];
|
||||
|
||||
bool get solvable => isSolvable(width, _intView);
|
||||
|
||||
Puzzle reset({List<int> source}) {
|
||||
final data = (source == null)
|
||||
? _randomizeList(width, _intView)
|
||||
: Uint8List.fromList(source);
|
||||
|
||||
if (data.length != length) {
|
||||
throw ArgumentError.value(source, 'source', 'Cannot change the size!');
|
||||
}
|
||||
_validate(data);
|
||||
if (!isSolvable(width, data)) {
|
||||
throw ArgumentError.value(source, 'source', 'Not a solvable puzzle.');
|
||||
}
|
||||
|
||||
return _newWithValues(data);
|
||||
}
|
||||
|
||||
int get incorrectTiles {
|
||||
var count = tileCount;
|
||||
for (var i = 0; i < tileCount; i++) {
|
||||
if (isCorrectPosition(i)) {
|
||||
count--;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
Point openPosition() => coordinatesOf(tileCount);
|
||||
|
||||
/// A measure of how close the puzzle is to being solved.
|
||||
///
|
||||
/// The sum of all of the distances squared `(x + y)^2 ` each tile has to move
|
||||
/// to be in the correct position.
|
||||
///
|
||||
/// `0` - you've won!
|
||||
int get fitness {
|
||||
var value = 0;
|
||||
for (var i = 0; i < tileCount; i++) {
|
||||
if (!isCorrectPosition(i)) {
|
||||
final correctColumn = i % width;
|
||||
final correctRow = i ~/ width;
|
||||
|
||||
final index = indexOf(i);
|
||||
final x = index % width;
|
||||
final y = index ~/ width;
|
||||
|
||||
final delta = (correctColumn - x).abs() + (correctRow - y).abs();
|
||||
|
||||
value += delta * delta;
|
||||
}
|
||||
}
|
||||
return value * incorrectTiles;
|
||||
}
|
||||
|
||||
Puzzle clickRandom({bool vertical}) {
|
||||
final clickable = clickableValues(vertical: vertical).toList();
|
||||
return clickValue(clickable[_rnd.nextInt(clickable.length)]);
|
||||
}
|
||||
|
||||
Iterable<Puzzle> allMovable() =>
|
||||
(clickableValues()..shuffle(_rnd)).map(_clickValue);
|
||||
|
||||
List<int> clickableValues({bool vertical}) {
|
||||
final open = openPosition();
|
||||
final doRow = vertical == null || vertical == false;
|
||||
final doColumn = vertical == null || vertical;
|
||||
|
||||
final values =
|
||||
Uint8List((doRow ? (width - 1) : 0) + (doColumn ? (height - 1) : 0));
|
||||
|
||||
var index = 0;
|
||||
|
||||
if (doRow) {
|
||||
for (var x = 0; x < width; x++) {
|
||||
if (x != open.x) {
|
||||
values[index++] = valueAt(x, open.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (doColumn) {
|
||||
for (var y = 0; y < height; y++) {
|
||||
if (y != open.y) {
|
||||
values[index++] = valueAt(open.x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
bool _movable(int tileValue) {
|
||||
if (tileValue == tileCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final target = coordinatesOf(tileValue);
|
||||
final lastCoord = openPosition();
|
||||
if (lastCoord.x != target.x && lastCoord.y != target.y) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Puzzle clickValue(int tileValue) {
|
||||
if (!_movable(tileValue)) {
|
||||
return null;
|
||||
}
|
||||
return _clickValue(tileValue);
|
||||
}
|
||||
|
||||
Puzzle _clickValue(int tileValue) {
|
||||
assert(_movable(tileValue));
|
||||
final target = coordinatesOf(tileValue);
|
||||
|
||||
final newStore = _copyData();
|
||||
|
||||
_shift(newStore, target.x, target.y);
|
||||
return _newWithValues(newStore);
|
||||
}
|
||||
|
||||
void _shift(List<int> source, int targetX, int targetY) {
|
||||
final lastCoord = openPosition();
|
||||
final deltaX = lastCoord.x - targetX;
|
||||
final deltaY = lastCoord.y - targetY;
|
||||
|
||||
if ((deltaX.abs() + deltaY.abs()) > 1) {
|
||||
final shiftPointX = targetX + deltaX.sign;
|
||||
final shiftPointY = targetY + deltaY.sign;
|
||||
_shift(source, shiftPointX, shiftPointY);
|
||||
_staticSwap(source, targetX, targetY, shiftPointX, shiftPointY);
|
||||
} else {
|
||||
_staticSwap(source, lastCoord.x, lastCoord.y, targetX, targetY);
|
||||
}
|
||||
}
|
||||
|
||||
void _staticSwap(List<int> source, int ax, int ay, int bx, int by) {
|
||||
final aIndex = ax + ay * width;
|
||||
final aValue = source[aIndex];
|
||||
final bIndex = bx + by * width;
|
||||
|
||||
source[aIndex] = source[bIndex];
|
||||
source[bIndex] = aValue;
|
||||
}
|
||||
|
||||
Point coordinatesOf(int value) {
|
||||
final index = indexOf(value);
|
||||
final x = index % width;
|
||||
final y = index ~/ width;
|
||||
assert(_getIndex(x, y) == index);
|
||||
return Point(x, y);
|
||||
}
|
||||
|
||||
int _getIndex(int x, int y) {
|
||||
assert(x >= 0 && x < width);
|
||||
assert(y >= 0 && y < height);
|
||||
return x + y * width;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => _toString();
|
||||
|
||||
String _toString() {
|
||||
final grid = List<List<String>>.generate(
|
||||
height,
|
||||
(row) => List<String>.generate(
|
||||
width, (col) => valueAt(col, row).toString()));
|
||||
|
||||
final longestLength =
|
||||
grid.expand((r) => r).fold(0, (int l, cell) => max(l, cell.length));
|
||||
|
||||
return grid
|
||||
.map((r) => r.map((v) => v.padLeft(longestLength)).join(' '))
|
||||
.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List _randomList(int width, int height) => _randomizeList(
|
||||
width, List<int>.generate(width * height, (i) => i, growable: false));
|
||||
|
||||
Uint8List _randomizeList(int width, List<int> existing) {
|
||||
final copy = Uint8List.fromList(existing);
|
||||
do {
|
||||
copy.shuffle(_rnd);
|
||||
} while (!isSolvable(width, copy) ||
|
||||
copy.any((v) => copy[v] == v || copy[v] == existing[v]));
|
||||
return copy;
|
||||
}
|
||||
|
||||
void _validate(List<int> source) {
|
||||
for (var i = 0; i < source.length; i++) {
|
||||
requireArgument(
|
||||
source.contains(i),
|
||||
'source',
|
||||
'Must contain each number from 0 to `length - 1` '
|
||||
'once and only once.');
|
||||
}
|
||||
}
|
||||
205
web/slide_puzzle/lib/src/core/puzzle_animator.dart
Normal file
205
web/slide_puzzle/lib/src/core/puzzle_animator.dart
Normal file
@@ -0,0 +1,205 @@
|
||||
// Copyright 2019 The Chromium Authors. 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:async';
|
||||
import 'dart:math' show Point, Random;
|
||||
|
||||
import 'body.dart';
|
||||
import 'puzzle.dart';
|
||||
|
||||
enum PuzzleEvent { click, reset, noop }
|
||||
|
||||
abstract class PuzzleProxy {
|
||||
int get width;
|
||||
|
||||
int get height;
|
||||
|
||||
int get length;
|
||||
|
||||
bool get solved;
|
||||
|
||||
void reset();
|
||||
|
||||
void clickOrShake(int tileValue);
|
||||
|
||||
int get tileCount;
|
||||
|
||||
int get clickCount;
|
||||
|
||||
int get incorrectTiles;
|
||||
|
||||
Point<double> location(int index);
|
||||
|
||||
bool isCorrectPosition(int value);
|
||||
}
|
||||
|
||||
class PuzzleAnimator implements PuzzleProxy {
|
||||
final _rnd = Random();
|
||||
final List<Body> _locations;
|
||||
final _controller = StreamController<PuzzleEvent>();
|
||||
bool _nextRandomVertical = true;
|
||||
Puzzle _puzzle;
|
||||
int _clickCount = 0;
|
||||
|
||||
bool _stable;
|
||||
|
||||
bool get stable => _stable;
|
||||
|
||||
@override
|
||||
bool get solved => _puzzle.incorrectTiles == 0;
|
||||
|
||||
@override
|
||||
int get width => _puzzle.width;
|
||||
|
||||
@override
|
||||
int get height => _puzzle.height;
|
||||
|
||||
@override
|
||||
int get length => _puzzle.length;
|
||||
|
||||
@override
|
||||
int get tileCount => _puzzle.tileCount;
|
||||
|
||||
@override
|
||||
int get incorrectTiles => _puzzle.incorrectTiles;
|
||||
|
||||
@override
|
||||
int get clickCount => _clickCount;
|
||||
|
||||
@override
|
||||
void reset() => _resetCore();
|
||||
|
||||
Stream<PuzzleEvent> get onEvent => _controller.stream;
|
||||
|
||||
@override
|
||||
bool isCorrectPosition(int value) => _puzzle.isCorrectPosition(value);
|
||||
|
||||
@override
|
||||
Point<double> location(int index) => _locations[index].location;
|
||||
|
||||
int _lastBadClick;
|
||||
int _badClickCount = 0;
|
||||
|
||||
PuzzleAnimator(int width, int height) : this._(Puzzle(width, height));
|
||||
|
||||
PuzzleAnimator._(this._puzzle)
|
||||
: _locations = List.generate(_puzzle.length, (i) {
|
||||
return Body.raw(
|
||||
(_puzzle.width - 1.0) / 2, (_puzzle.height - 1.0) / 2, 0, 0);
|
||||
});
|
||||
|
||||
void playRandom() {
|
||||
if (_puzzle.fitness == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
_puzzle = _puzzle.clickRandom(vertical: _nextRandomVertical);
|
||||
_nextRandomVertical = !_nextRandomVertical;
|
||||
_clickCount++;
|
||||
_controller.add(PuzzleEvent.click);
|
||||
}
|
||||
|
||||
@override
|
||||
void clickOrShake(int tileValue) {
|
||||
if (solved) {
|
||||
_controller.add(PuzzleEvent.noop);
|
||||
_shake(tileValue);
|
||||
_lastBadClick = null;
|
||||
_badClickCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
_controller.add(PuzzleEvent.click);
|
||||
if (!_clickValue(tileValue)) {
|
||||
_shake(tileValue);
|
||||
|
||||
// This is logic to allow a user to skip to the end – useful for testing
|
||||
// click on 5 un-movable tiles in a row, but not the same tile twice
|
||||
// in a row
|
||||
if (tileValue != _lastBadClick) {
|
||||
_badClickCount++;
|
||||
if (_badClickCount >= 5) {
|
||||
// Do the reset!
|
||||
final newValues = List.generate(_puzzle.length, (i) {
|
||||
if (i == _puzzle.tileCount) {
|
||||
return _puzzle.tileCount - 1;
|
||||
} else if (i == (_puzzle.tileCount - 1)) {
|
||||
return _puzzle.tileCount;
|
||||
}
|
||||
return i;
|
||||
});
|
||||
_resetCore(source: newValues);
|
||||
_clickCount = 999;
|
||||
}
|
||||
} else {
|
||||
_badClickCount = 0;
|
||||
}
|
||||
_lastBadClick = tileValue;
|
||||
} else {
|
||||
_lastBadClick = null;
|
||||
_badClickCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void _resetCore({List<int> source}) {
|
||||
_puzzle = _puzzle.reset(source: source);
|
||||
_clickCount = 0;
|
||||
_lastBadClick = null;
|
||||
_badClickCount = 0;
|
||||
_controller.add(PuzzleEvent.reset);
|
||||
}
|
||||
|
||||
bool _clickValue(int value) {
|
||||
final newPuzzle = _puzzle.clickValue(value);
|
||||
if (newPuzzle == null) {
|
||||
return false;
|
||||
} else {
|
||||
_clickCount++;
|
||||
_puzzle = newPuzzle;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void _shake(int tileValue) {
|
||||
Point<double> deltaDouble;
|
||||
if (solved) {
|
||||
deltaDouble = Point(_rnd.nextDouble() - 0.5, _rnd.nextDouble() - 0.5);
|
||||
} else {
|
||||
final delta = _puzzle.openPosition() - _puzzle.coordinatesOf(tileValue);
|
||||
deltaDouble = Point(delta.x.toDouble(), delta.y.toDouble());
|
||||
}
|
||||
deltaDouble *= 0.5 / deltaDouble.magnitude;
|
||||
|
||||
_locations[tileValue].kick(deltaDouble);
|
||||
}
|
||||
|
||||
void update(Duration timeDelta) {
|
||||
assert(!timeDelta.isNegative);
|
||||
assert(timeDelta != Duration.zero);
|
||||
|
||||
var animationSeconds = timeDelta.inMilliseconds / 60.0;
|
||||
if (animationSeconds == 0) {
|
||||
animationSeconds = 0.1;
|
||||
}
|
||||
assert(animationSeconds > 0);
|
||||
|
||||
_stable = true;
|
||||
for (var i = 0; i < _puzzle.length; i++) {
|
||||
final target = _target(i);
|
||||
final body = _locations[i];
|
||||
|
||||
_stable = !body.animate(animationSeconds,
|
||||
force: target - body.location,
|
||||
drag: .9,
|
||||
maxVelocity: 1.0,
|
||||
snapTo: target) &&
|
||||
_stable;
|
||||
}
|
||||
}
|
||||
|
||||
Point<double> _target(int item) {
|
||||
final target = _puzzle.coordinatesOf(item);
|
||||
return Point(target.x.toDouble(), target.y.toDouble());
|
||||
}
|
||||
}
|
||||
63
web/slide_puzzle/lib/src/core/puzzle_simple.dart
Normal file
63
web/slide_puzzle/lib/src/core/puzzle_simple.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
part of 'puzzle.dart';
|
||||
|
||||
class _PuzzleSimple extends Puzzle {
|
||||
@override
|
||||
final int width;
|
||||
final Uint8List _source;
|
||||
|
||||
_PuzzleSimple(this.width, List<int> source)
|
||||
: _source = UnmodifiableUint8ListView(Uint8List.fromList(source)),
|
||||
super._();
|
||||
|
||||
@override
|
||||
int indexOf(int value) => _source.indexOf(value);
|
||||
|
||||
@override
|
||||
List<int> get _intView => _source;
|
||||
|
||||
@override
|
||||
List<int> _copyData() => Uint8List.fromList(_source);
|
||||
|
||||
@override
|
||||
Puzzle _newWithValues(List<int> values) => _PuzzleSimple(width, values);
|
||||
|
||||
@override
|
||||
int operator [](int index) => _source[index];
|
||||
|
||||
@override
|
||||
Puzzle clone() => _PuzzleSimple(width, _source);
|
||||
|
||||
@override
|
||||
int get length => _source.length;
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is Puzzle &&
|
||||
other.width == width &&
|
||||
other.length == _source.length) {
|
||||
for (var i = 0; i < _source.length; i++) {
|
||||
if (other[i] != _source[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var v = 0;
|
||||
for (var i = 0; i < _source.length; i++) {
|
||||
v = (v << 2) + _source[i];
|
||||
}
|
||||
v += v << 3;
|
||||
v ^= v >> 11;
|
||||
v += v << 15;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
188
web/slide_puzzle/lib/src/core/puzzle_smart.dart
Normal file
188
web/slide_puzzle/lib/src/core/puzzle_smart.dart
Normal file
@@ -0,0 +1,188 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
part of 'puzzle.dart';
|
||||
|
||||
mixin _SliceListMixin on ListMixin<int> {
|
||||
static const _bitsPerValue = 4;
|
||||
static const _maxShift = _valuesPerCell - 1;
|
||||
|
||||
static const _bitsPerCell = 32;
|
||||
static const _valuesPerCell = _bitsPerCell ~/ _bitsPerValue;
|
||||
static const _valueMask = (1 << _bitsPerValue) - 1;
|
||||
|
||||
Uint32List get _data;
|
||||
|
||||
int _cellValue(int index) => _data[index ~/ _valuesPerCell];
|
||||
|
||||
@override
|
||||
int operator [](int index) {
|
||||
/*
|
||||
final bigValue = _data[index ~/ _valuesPerCell];
|
||||
final shiftedValue =
|
||||
bigValue >> (_maxShift - (index % _valuesPerCell)) * _bitsPerValue;
|
||||
final flattenedValue = shiftedValue & _valueMask;
|
||||
return flattenedValue;
|
||||
*/
|
||||
return (_cellValue(index) >>
|
||||
(_maxShift - (index % _valuesPerCell)) * _bitsPerValue) &
|
||||
_valueMask;
|
||||
}
|
||||
|
||||
@override
|
||||
int indexOf(Object value, [int start]) {
|
||||
for (var i = 0; i < _data.length; i++) {
|
||||
final cellValue = _data[i];
|
||||
for (var j = 0; j < _valuesPerCell; j++) {
|
||||
final option =
|
||||
(cellValue >> (_maxShift - j) * _bitsPerValue) & _valueMask;
|
||||
|
||||
if (value == option) {
|
||||
final k = i * _valuesPerCell + j;
|
||||
if (k < length && (start == null || k >= start)) {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@override
|
||||
set length(int value) => throw UnsupportedError('immutable, yo!');
|
||||
}
|
||||
|
||||
class _SliceList with ListMixin<int>, _SliceListMixin {
|
||||
@override
|
||||
final Uint32List _data;
|
||||
|
||||
@override
|
||||
final int length;
|
||||
|
||||
_SliceList(this.length, this._data);
|
||||
|
||||
@override
|
||||
void operator []=(int index, int value) {
|
||||
var cellValue = _cellValue(index);
|
||||
|
||||
// clear out the target bits in `cellValue`
|
||||
|
||||
final sharedShift =
|
||||
(_SliceListMixin._maxShift - (index % _SliceListMixin._valuesPerCell)) *
|
||||
_SliceListMixin._bitsPerValue;
|
||||
|
||||
final wipeout = _SliceListMixin._valueMask << sharedShift;
|
||||
|
||||
cellValue &= ~wipeout;
|
||||
|
||||
final newShiftedValue = value << sharedShift;
|
||||
|
||||
cellValue |= newShiftedValue;
|
||||
|
||||
_data[index ~/ _SliceListMixin._valuesPerCell] = cellValue;
|
||||
}
|
||||
}
|
||||
|
||||
class _PuzzleSmart extends Puzzle with ListMixin<int>, _SliceListMixin {
|
||||
static const _bitsPerValue = 4;
|
||||
static const _maxShift = _valuesPerCell - 1;
|
||||
|
||||
static const _bitsPerCell = 32;
|
||||
static const _valuesPerCell = _bitsPerCell ~/ _bitsPerValue;
|
||||
static const _valueMask = (1 << _bitsPerValue) - 1;
|
||||
|
||||
@override
|
||||
final Uint32List _data;
|
||||
|
||||
@override
|
||||
final int width;
|
||||
|
||||
@override
|
||||
final int length;
|
||||
|
||||
int _fitnessCache;
|
||||
|
||||
@override
|
||||
int get fitness => _fitnessCache ??= super.fitness;
|
||||
|
||||
_PuzzleSmart(this.width, List<int> source)
|
||||
: length = source.length,
|
||||
_data = _create(source),
|
||||
super._();
|
||||
|
||||
@override
|
||||
void operator []=(int index, int value) =>
|
||||
throw UnsupportedError('immutable, yo!');
|
||||
|
||||
@override
|
||||
List<int> get _intView => this;
|
||||
|
||||
@override
|
||||
List<int> _copyData() => _SliceList(length, Uint32List.fromList(_data));
|
||||
|
||||
@override
|
||||
Puzzle _newWithValues(List<int> values) => _PuzzleSmart(width, values);
|
||||
|
||||
@override
|
||||
Puzzle clone() => _PuzzleSmart(width, this);
|
||||
|
||||
@override
|
||||
String toString() => _toString();
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is _PuzzleSmart &&
|
||||
other.width == width &&
|
||||
other._data.length == _data.length) {
|
||||
for (var i = 0; i < _data.length; i++) {
|
||||
if (other._data[i] != _data[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (other is Puzzle && other.width == width && other.length == length) {
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (other[i] != this[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var v = 0;
|
||||
for (var i = 0; i < _data.length; i++) {
|
||||
v = (v << 2) + _data[i];
|
||||
}
|
||||
v += v << 3;
|
||||
v ^= v >> 11;
|
||||
v += v << 15;
|
||||
return v;
|
||||
}
|
||||
|
||||
static Uint32List _create(List<int> source) {
|
||||
if (source is _SliceList) {
|
||||
return source._data;
|
||||
}
|
||||
|
||||
final data = Uint32List((source.length / _valuesPerCell).ceil());
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var value = 0;
|
||||
for (var j = 0; j < _valuesPerCell; j++) {
|
||||
final k = i * _valuesPerCell + j;
|
||||
if (k < source.length) {
|
||||
// shift the value over 4 bits for item 0, 3 for item 2, etc
|
||||
final sourceValue = source[k] << ((_maxShift - j) * _bitsPerValue);
|
||||
value |= sourceValue;
|
||||
}
|
||||
}
|
||||
data[i] = value;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
58
web/slide_puzzle/lib/src/core/util.dart
Normal file
58
web/slide_puzzle/lib/src/core/util.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
void requireArgument(bool truth, String argName, [String message]) {
|
||||
if (!truth) {
|
||||
if (message == null || message.isEmpty) {
|
||||
message = 'value was invalid';
|
||||
}
|
||||
throw ArgumentError('`$argName` - $message');
|
||||
}
|
||||
}
|
||||
|
||||
void requireArgumentNotNull(argument, String argName) {
|
||||
if (argument == null) {
|
||||
throw ArgumentError.notNull(argName);
|
||||
}
|
||||
}
|
||||
|
||||
// Logic from
|
||||
// https://www.cs.bham.ac.uk/~mdr/teaching/modules04/java2/TilesSolvability.html
|
||||
// Used with gratitude!
|
||||
bool isSolvable(int width, List<int> list) {
|
||||
final height = list.length ~/ width;
|
||||
assert(width * height == list.length);
|
||||
final inversions = _countInversions(list);
|
||||
|
||||
if (width.isOdd) {
|
||||
return inversions.isEven;
|
||||
}
|
||||
|
||||
final blankRow = list.indexOf(list.length - 1) ~/ width;
|
||||
|
||||
if ((height - blankRow).isEven) {
|
||||
return inversions.isOdd;
|
||||
} else {
|
||||
return inversions.isEven;
|
||||
}
|
||||
}
|
||||
|
||||
int _countInversions(List<int> items) {
|
||||
final tileCount = items.length - 1;
|
||||
var score = 0;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
final value = items[i];
|
||||
if (value == tileCount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var j = i + 1; j < items.length; j++) {
|
||||
final v = items[j];
|
||||
if (v != tileCount && v < value) {
|
||||
score++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return score;
|
||||
}
|
||||
7
web/slide_puzzle/lib/src/flutter.dart
Normal file
7
web/slide_puzzle/lib/src/flutter.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
export 'package:flutter_web/material.dart';
|
||||
|
||||
export 'package:flutter_web/scheduler.dart' show Ticker;
|
||||
43
web/slide_puzzle/lib/src/puzzle_flow_delegate.dart
Normal file
43
web/slide_puzzle/lib/src/puzzle_flow_delegate.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'core/puzzle_animator.dart';
|
||||
import 'flutter.dart';
|
||||
|
||||
class PuzzleFlowDelegate extends FlowDelegate {
|
||||
final Size _tileSize;
|
||||
final PuzzleProxy _puzzleProxy;
|
||||
|
||||
PuzzleFlowDelegate(this._tileSize, this._puzzleProxy, Listenable repaint)
|
||||
: super(repaint: repaint);
|
||||
|
||||
@override
|
||||
Size getSize(BoxConstraints constraints) => Size(
|
||||
_tileSize.width * _puzzleProxy.width,
|
||||
_tileSize.height * _puzzleProxy.height);
|
||||
|
||||
@override
|
||||
BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) =>
|
||||
BoxConstraints.tight(_tileSize);
|
||||
|
||||
@override
|
||||
void paintChildren(FlowPaintingContext context) {
|
||||
for (var i = 0; i < _puzzleProxy.length; i++) {
|
||||
final tileLocation = _puzzleProxy.location(i);
|
||||
context.paintChild(
|
||||
i,
|
||||
transform: Matrix4.translationValues(
|
||||
tileLocation.x * _tileSize.width,
|
||||
tileLocation.y * _tileSize.height,
|
||||
i.toDouble(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant PuzzleFlowDelegate oldDelegate) =>
|
||||
_tileSize != oldDelegate._tileSize ||
|
||||
!identical(oldDelegate._puzzleProxy, _puzzleProxy);
|
||||
}
|
||||
177
web/slide_puzzle/lib/src/puzzle_home_state.dart
Normal file
177
web/slide_puzzle/lib/src/puzzle_home_state.dart
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright 2019 The Chromium Authors. 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:async';
|
||||
|
||||
import 'app_state.dart';
|
||||
import 'core/puzzle_animator.dart';
|
||||
import 'flutter.dart';
|
||||
import 'shared_theme.dart';
|
||||
import 'theme_plaster.dart';
|
||||
import 'theme_seattle.dart';
|
||||
import 'theme_simple.dart';
|
||||
|
||||
class PuzzleHomeState extends State
|
||||
with TickerProviderStateMixin
|
||||
implements AppState {
|
||||
TabController _tabController;
|
||||
AnimationController _controller;
|
||||
|
||||
@override
|
||||
final PuzzleAnimator puzzle;
|
||||
|
||||
@override
|
||||
final animationNotifier = _AnimationNotifier();
|
||||
|
||||
@override
|
||||
TabController get tabController => _tabController;
|
||||
|
||||
SharedTheme _currentTheme;
|
||||
|
||||
@override
|
||||
SharedTheme get currentTheme => _currentTheme;
|
||||
|
||||
@override
|
||||
set currentTheme(SharedTheme theme) {
|
||||
setState(() {
|
||||
_currentTheme = theme;
|
||||
});
|
||||
}
|
||||
|
||||
Duration _tickerTimeSinceLastEvent = Duration.zero;
|
||||
Ticker _ticker;
|
||||
Duration _lastElapsed;
|
||||
StreamSubscription sub;
|
||||
|
||||
@override
|
||||
bool autoPlay = false;
|
||||
|
||||
PuzzleHomeState(this.puzzle) {
|
||||
sub = puzzle.onEvent.listen(_onPuzzleEvent);
|
||||
|
||||
_themeDataCache = List.unmodifiable([
|
||||
ThemeSimple(this),
|
||||
ThemeSeattle(this),
|
||||
ThemePlaster(this),
|
||||
]);
|
||||
|
||||
_currentTheme = themeData.first;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ticker ??= createTicker(_onTick);
|
||||
_ensureTicking();
|
||||
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
|
||||
_tabController = TabController(vsync: this, length: _themeDataCache.length);
|
||||
|
||||
_tabController.addListener(() {
|
||||
currentTheme = _themeDataCache[_tabController.index];
|
||||
});
|
||||
}
|
||||
|
||||
List<SharedTheme> _themeDataCache;
|
||||
|
||||
@override
|
||||
Iterable<SharedTheme> get themeData => _themeDataCache;
|
||||
|
||||
@override
|
||||
void setAutoPlay(bool newValue) {
|
||||
if (newValue != autoPlay) {
|
||||
setState(() {
|
||||
// Only allow enabling autoPlay if the puzzle is not solved
|
||||
autoPlay = newValue && !puzzle.solved;
|
||||
if (autoPlay) {
|
||||
_ensureTicking();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
LayoutBuilder(builder: _currentTheme.build);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animationNotifier.dispose();
|
||||
_tabController.dispose();
|
||||
_controller?.dispose();
|
||||
_ticker?.dispose();
|
||||
sub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onPuzzleEvent(PuzzleEvent e) {
|
||||
_tickerTimeSinceLastEvent = Duration.zero;
|
||||
_ensureTicking();
|
||||
if (e == PuzzleEvent.noop) {
|
||||
assert(e == PuzzleEvent.noop);
|
||||
_controller
|
||||
..reset()
|
||||
..forward();
|
||||
}
|
||||
setState(() {
|
||||
// noop
|
||||
});
|
||||
}
|
||||
|
||||
void _ensureTicking() {
|
||||
if (!_ticker.isTicking) {
|
||||
_ticker.start();
|
||||
}
|
||||
}
|
||||
|
||||
void _onTick(Duration elapsed) {
|
||||
if (elapsed == Duration.zero) {
|
||||
_lastElapsed = elapsed;
|
||||
}
|
||||
final delta = elapsed - _lastElapsed;
|
||||
_lastElapsed = elapsed;
|
||||
|
||||
if (delta.inMilliseconds <= 0) {
|
||||
// `_delta` may be negative or zero if `elapsed` is zero (first tick)
|
||||
// or during a restart. Just ignore this case.
|
||||
return;
|
||||
}
|
||||
|
||||
_tickerTimeSinceLastEvent += delta;
|
||||
puzzle.update(delta > _maxFrameDuration ? _maxFrameDuration : delta);
|
||||
|
||||
if (!puzzle.stable) {
|
||||
animationNotifier.animate();
|
||||
} else {
|
||||
if (!autoPlay) {
|
||||
_ticker.stop();
|
||||
_lastElapsed = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (autoPlay &&
|
||||
_tickerTimeSinceLastEvent > const Duration(milliseconds: 200)) {
|
||||
puzzle.playRandom();
|
||||
|
||||
if (puzzle.solved) {
|
||||
setAutoPlay(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimationNotifier extends ChangeNotifier implements AnimationNotifier {
|
||||
_AnimationNotifier();
|
||||
|
||||
@override
|
||||
void animate() {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
const _maxFrameDuration = Duration(milliseconds: 34);
|
||||
247
web/slide_puzzle/lib/src/shared_theme.dart
Normal file
247
web/slide_puzzle/lib/src/shared_theme.dart
Normal file
@@ -0,0 +1,247 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'app_state.dart';
|
||||
import 'core/puzzle_animator.dart';
|
||||
import 'flutter.dart';
|
||||
import 'puzzle_flow_delegate.dart';
|
||||
import 'widgets/material_interior_alt.dart';
|
||||
|
||||
abstract class SharedTheme {
|
||||
SharedTheme(this._appState);
|
||||
|
||||
final AppState _appState;
|
||||
|
||||
PuzzleProxy get puzzle => _appState.puzzle;
|
||||
|
||||
String get name;
|
||||
|
||||
Color get puzzleThemeBackground;
|
||||
|
||||
RoundedRectangleBorder get puzzleBorder;
|
||||
|
||||
Color get puzzleBackgroundColor;
|
||||
|
||||
Color get puzzleAccentColor;
|
||||
|
||||
EdgeInsetsGeometry get tilePadding => const EdgeInsets.all(6);
|
||||
|
||||
Widget tileButton(int i);
|
||||
|
||||
Ink createInk(
|
||||
Widget child, {
|
||||
DecorationImage image,
|
||||
EdgeInsetsGeometry padding,
|
||||
}) =>
|
||||
Ink(
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
image: image,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
||||
Widget createButton(
|
||||
int tileValue,
|
||||
Widget content, {
|
||||
Color color,
|
||||
RoundedRectangleBorder shape,
|
||||
}) =>
|
||||
AnimatedContainer(
|
||||
duration: _puzzleAnimationDuration,
|
||||
padding: tilePadding,
|
||||
child: RaisedButton(
|
||||
elevation: 4,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
animationDuration: _puzzleAnimationDuration,
|
||||
onPressed: () => _tilePress(tileValue),
|
||||
shape: shape ?? puzzleBorder,
|
||||
padding: const EdgeInsets.symmetric(),
|
||||
child: content,
|
||||
color: color,
|
||||
),
|
||||
);
|
||||
|
||||
double _previousConstraintWidth;
|
||||
bool _small;
|
||||
|
||||
bool get small => _small;
|
||||
|
||||
void _updateConstraints(BoxConstraints constraints) {
|
||||
const _smallWidth = 580;
|
||||
|
||||
final constraintWidth =
|
||||
constraints.hasBoundedWidth ? constraints.maxWidth : 1000.0;
|
||||
|
||||
if (constraintWidth == _previousConstraintWidth) {
|
||||
assert(_small != null);
|
||||
return;
|
||||
}
|
||||
|
||||
_previousConstraintWidth = constraintWidth;
|
||||
|
||||
if (_previousConstraintWidth < _smallWidth) {
|
||||
_small = true;
|
||||
} else {
|
||||
_small = false;
|
||||
}
|
||||
}
|
||||
|
||||
Widget build(BuildContext context, BoxConstraints constraints) {
|
||||
_updateConstraints(constraints);
|
||||
return Material(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
const SizedBox.expand(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: Image(
|
||||
image: AssetImage('seattle.jpg'),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: _puzzleAnimationDuration,
|
||||
color: puzzleThemeBackground,
|
||||
child: Center(
|
||||
child: _styledWrapper(
|
||||
SizedBox(
|
||||
width: 580,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.black26,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: TabBar(
|
||||
controller: _appState.tabController,
|
||||
labelPadding: const EdgeInsets.fromLTRB(0, 20, 0, 12),
|
||||
labelColor: puzzleAccentColor,
|
||||
indicatorColor: puzzleAccentColor,
|
||||
indicatorWeight: 1.5,
|
||||
unselectedLabelColor: Colors.black.withOpacity(0.6),
|
||||
tabs: _appState.themeData
|
||||
.map((st) => Text(
|
||||
st.name.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
constraints: const BoxConstraints.tightForFinite(),
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Flow(
|
||||
delegate: PuzzleFlowDelegate(
|
||||
small ? const Size(90, 90) : const Size(140, 140),
|
||||
puzzle,
|
||||
_appState.animationNotifier,
|
||||
),
|
||||
children: List<Widget>.generate(
|
||||
puzzle.length,
|
||||
_tileButton,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.black26, width: 1),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10,
|
||||
bottom: 6,
|
||||
top: 2,
|
||||
right: 10,
|
||||
),
|
||||
child: Row(children: _bottomControls(context)),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Duration get _puzzleAnimationDuration => kThemeAnimationDuration * 3;
|
||||
|
||||
// Thought about using AnimatedContainer here, but it causes some weird
|
||||
// resizing behavior
|
||||
Widget _styledWrapper(Widget child) => MaterialInterior(
|
||||
duration: _puzzleAnimationDuration,
|
||||
shape: puzzleBorder,
|
||||
color: puzzleBackgroundColor,
|
||||
child: child,
|
||||
);
|
||||
|
||||
void Function(bool newValue) get _setAutoPlay {
|
||||
if (puzzle.solved) {
|
||||
return null;
|
||||
}
|
||||
return _appState.setAutoPlay;
|
||||
}
|
||||
|
||||
void _tilePress(int tileValue) {
|
||||
_appState.setAutoPlay(false);
|
||||
_appState.puzzle.clickOrShake(tileValue);
|
||||
}
|
||||
|
||||
TextStyle get _infoStyle => TextStyle(
|
||||
color: puzzleAccentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
|
||||
List<Widget> _bottomControls(BuildContext context) => <Widget>[
|
||||
IconButton(
|
||||
onPressed: puzzle.reset,
|
||||
icon: Icon(Icons.refresh, color: puzzleAccentColor),
|
||||
//Icons.refresh,
|
||||
),
|
||||
Checkbox(
|
||||
value: _appState.autoPlay,
|
||||
onChanged: _setAutoPlay,
|
||||
activeColor: puzzleAccentColor,
|
||||
),
|
||||
Expanded(
|
||||
child: Container(),
|
||||
),
|
||||
Text(
|
||||
puzzle.clickCount.toString(),
|
||||
textAlign: TextAlign.right,
|
||||
style: _infoStyle,
|
||||
),
|
||||
const Text(' Moves'),
|
||||
SizedBox(
|
||||
width: 28,
|
||||
child: Text(
|
||||
puzzle.incorrectTiles.toString(),
|
||||
textAlign: TextAlign.right,
|
||||
style: _infoStyle,
|
||||
),
|
||||
),
|
||||
const Text(' Tiles left ')
|
||||
];
|
||||
|
||||
Widget _tileButton(int i) {
|
||||
if (i == puzzle.tileCount && !puzzle.solved) {
|
||||
return const Center();
|
||||
}
|
||||
|
||||
return tileButton(i);
|
||||
}
|
||||
}
|
||||
76
web/slide_puzzle/lib/src/theme_plaster.dart
Normal file
76
web/slide_puzzle/lib/src/theme_plaster.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'app_state.dart';
|
||||
import 'flutter.dart';
|
||||
import 'shared_theme.dart';
|
||||
|
||||
const _yellowIsh = Color.fromARGB(255, 248, 244, 233);
|
||||
const _chocolate = Color.fromARGB(255, 66, 66, 68);
|
||||
const _orangeIsh = Color.fromARGB(255, 224, 107, 83);
|
||||
|
||||
class ThemePlaster extends SharedTheme {
|
||||
@override
|
||||
String get name => 'Plaster';
|
||||
|
||||
ThemePlaster(AppState baseTheme) : super(baseTheme);
|
||||
|
||||
@override
|
||||
Color get puzzleThemeBackground => _chocolate;
|
||||
|
||||
@override
|
||||
Color get puzzleBackgroundColor => _yellowIsh;
|
||||
|
||||
@override
|
||||
Color get puzzleAccentColor => _orangeIsh;
|
||||
|
||||
@override
|
||||
RoundedRectangleBorder get puzzleBorder => RoundedRectangleBorder(
|
||||
side: const BorderSide(
|
||||
color: Color.fromARGB(255, 103, 103, 105),
|
||||
width: 8,
|
||||
),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(small ? 10 : 18),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget tileButton(int i) {
|
||||
final correctColumn = i % puzzle.width;
|
||||
final correctRow = i ~/ puzzle.width;
|
||||
|
||||
final primary = (correctColumn + correctRow).isEven;
|
||||
|
||||
if (i == puzzle.tileCount) {
|
||||
assert(puzzle.solved);
|
||||
return Center(
|
||||
child: Icon(
|
||||
Icons.thumb_up,
|
||||
size: small ? 50 : 72,
|
||||
color: _orangeIsh,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final content = Text(
|
||||
(i + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: primary ? _yellowIsh : _chocolate,
|
||||
fontFamily: 'Plaster',
|
||||
fontSize: small ? 40 : 77,
|
||||
),
|
||||
);
|
||||
|
||||
return createButton(
|
||||
i,
|
||||
content,
|
||||
color: primary ? _orangeIsh : _yellowIsh,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: primary ? _chocolate : _orangeIsh, width: 5),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
74
web/slide_puzzle/lib/src/theme_seattle.dart
Normal file
74
web/slide_puzzle/lib/src/theme_seattle.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'app_state.dart';
|
||||
import 'flutter.dart';
|
||||
import 'shared_theme.dart';
|
||||
import 'widgets/decoration_image_plus.dart';
|
||||
|
||||
class ThemeSeattle extends SharedTheme {
|
||||
@override
|
||||
String get name => 'Seattle';
|
||||
|
||||
ThemeSeattle(AppState proxy) : super(proxy);
|
||||
|
||||
@override
|
||||
Color get puzzleThemeBackground => const Color.fromARGB(153, 90, 135, 170);
|
||||
|
||||
@override
|
||||
Color get puzzleBackgroundColor => Colors.white70;
|
||||
|
||||
@override
|
||||
Color get puzzleAccentColor => const Color(0xff000579f);
|
||||
|
||||
@override
|
||||
RoundedRectangleBorder get puzzleBorder => const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(1),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
EdgeInsetsGeometry get tilePadding =>
|
||||
puzzle.solved ? const EdgeInsets.all(1) : const EdgeInsets.all(4);
|
||||
|
||||
@override
|
||||
Widget tileButton(int i) {
|
||||
if (i == puzzle.tileCount && !puzzle.solved) {
|
||||
assert(puzzle.solved);
|
||||
}
|
||||
|
||||
final decorationImage = DecorationImagePlus(
|
||||
puzzleWidth: puzzle.width,
|
||||
puzzleHeight: puzzle.height,
|
||||
pieceIndex: i,
|
||||
fit: BoxFit.cover,
|
||||
image: const AssetImage('seattle.jpg'));
|
||||
|
||||
final correctPosition = puzzle.isCorrectPosition(i);
|
||||
final content = createInk(
|
||||
puzzle.solved
|
||||
? const Center()
|
||||
: Container(
|
||||
decoration: ShapeDecoration(
|
||||
shape: const CircleBorder(),
|
||||
color: correctPosition ? Colors.black38 : Colors.white54,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
(i + 1).toString(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
color: correctPosition ? Colors.white : Colors.black,
|
||||
fontSize: small ? 25 : 42,
|
||||
),
|
||||
),
|
||||
),
|
||||
image: decorationImage,
|
||||
padding: EdgeInsets.all(small ? 20 : 32),
|
||||
);
|
||||
|
||||
return createButton(i, content);
|
||||
}
|
||||
}
|
||||
68
web/slide_puzzle/lib/src/theme_simple.dart
Normal file
68
web/slide_puzzle/lib/src/theme_simple.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'app_state.dart';
|
||||
import 'flutter.dart';
|
||||
import 'shared_theme.dart';
|
||||
|
||||
const _accentBlue = Color(0xff000579e);
|
||||
|
||||
class ThemeSimple extends SharedTheme {
|
||||
@override
|
||||
String get name => 'Simple';
|
||||
|
||||
ThemeSimple(AppState proxy) : super(proxy);
|
||||
|
||||
@override
|
||||
Color get puzzleThemeBackground => Colors.white;
|
||||
|
||||
@override
|
||||
Color get puzzleBackgroundColor => Colors.white70;
|
||||
|
||||
@override
|
||||
Color get puzzleAccentColor => _accentBlue;
|
||||
|
||||
@override
|
||||
RoundedRectangleBorder get puzzleBorder => const RoundedRectangleBorder(
|
||||
side: BorderSide(color: Colors.black26, width: 1),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(4),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget tileButton(int i) {
|
||||
if (i == puzzle.tileCount) {
|
||||
assert(puzzle.solved);
|
||||
return const Center(
|
||||
child: Icon(
|
||||
Icons.thumb_up,
|
||||
size: 72,
|
||||
color: _accentBlue,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final correctPosition = puzzle.isCorrectPosition(i);
|
||||
|
||||
final content = createInk(
|
||||
Center(
|
||||
child: Text(
|
||||
(i + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: correctPosition ? FontWeight.bold : FontWeight.normal,
|
||||
fontSize: small ? 30 : 49,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return createButton(
|
||||
i,
|
||||
content,
|
||||
color: const Color.fromARGB(255, 13, 87, 155),
|
||||
);
|
||||
}
|
||||
}
|
||||
322
web/slide_puzzle/lib/src/widgets/decoration_image_plus.dart
Normal file
322
web/slide_puzzle/lib/src/widgets/decoration_image_plus.dart
Normal file
@@ -0,0 +1,322 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: omit_local_variable_types, annotate_overrides
|
||||
|
||||
import 'package:flutter_web_ui/ui.dart' as ui show Image;
|
||||
|
||||
import '../flutter.dart';
|
||||
|
||||
// A model on top of DecorationImage that supports slicing up the source image
|
||||
// efficiently to draw it as tiles in the puzzle game
|
||||
@immutable
|
||||
class DecorationImagePlus implements DecorationImage {
|
||||
final int puzzleWidth, puzzleHeight, pieceIndex;
|
||||
|
||||
/// Creates an image to show in a [BoxDecoration].
|
||||
///
|
||||
/// The [image], [alignment], [repeat], and [matchTextDirection] arguments
|
||||
/// must not be null.
|
||||
const DecorationImagePlus({
|
||||
@required this.image,
|
||||
@required this.puzzleWidth,
|
||||
@required this.puzzleHeight,
|
||||
@required this.pieceIndex,
|
||||
this.colorFilter,
|
||||
this.fit,
|
||||
this.alignment = Alignment.center,
|
||||
this.centerSlice,
|
||||
this.repeat = ImageRepeat.noRepeat,
|
||||
this.matchTextDirection = false,
|
||||
}) : assert(image != null),
|
||||
assert(alignment != null),
|
||||
assert(repeat != null),
|
||||
assert(matchTextDirection != null),
|
||||
assert(puzzleHeight > 1 &&
|
||||
puzzleHeight > 1 &&
|
||||
pieceIndex >= 0 &&
|
||||
pieceIndex < (puzzleHeight * puzzleWidth));
|
||||
|
||||
/// The image to be painted into the decoration.
|
||||
///
|
||||
/// Typically this will be an [AssetImage] (for an image shipped with the
|
||||
/// application) or a [NetworkImage] (for an image obtained from the network).
|
||||
final ImageProvider image;
|
||||
|
||||
/// A color filter to apply to the image before painting it.
|
||||
final ColorFilter colorFilter;
|
||||
|
||||
/// How the image should be inscribed into the box.
|
||||
///
|
||||
/// The default is [BoxFit.scaleDown] if [centerSlice] is null, and
|
||||
/// [BoxFit.fill] if [centerSlice] is not null.
|
||||
///
|
||||
/// See the discussion at [_paintImage] for more details.
|
||||
final BoxFit fit;
|
||||
|
||||
/// How to align the image within its bounds.
|
||||
///
|
||||
/// The alignment aligns the given position in the image to the given position
|
||||
/// in the layout bounds. For example, an [Alignment] alignment of (-1.0,
|
||||
/// -1.0) aligns the image to the top-left corner of its layout bounds, while a
|
||||
/// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
|
||||
/// image with the bottom right corner of its layout bounds. Similarly, an
|
||||
/// alignment of (0.0, 1.0) aligns the bottom middle of the image with the
|
||||
/// middle of the bottom edge of its layout bounds.
|
||||
///
|
||||
/// To display a subpart of an image, consider using a [CustomPainter] and
|
||||
/// [Canvas.drawImageRect].
|
||||
///
|
||||
/// If the [alignment] is [TextDirection]-dependent (i.e. if it is a
|
||||
/// [AlignmentDirectional]), then a [TextDirection] must be available
|
||||
/// when the image is painted.
|
||||
///
|
||||
/// Defaults to [Alignment.center].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Alignment], a class with convenient constants typically used to
|
||||
/// specify an [AlignmentGeometry].
|
||||
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
|
||||
/// relative to text direction.
|
||||
final AlignmentGeometry alignment;
|
||||
|
||||
/// The center slice for a nine-patch image.
|
||||
///
|
||||
/// The region of the image inside the center slice will be stretched both
|
||||
/// horizontally and vertically to fit the image into its destination. The
|
||||
/// region of the image above and below the center slice will be stretched
|
||||
/// only horizontally and the region of the image to the left and right of
|
||||
/// the center slice will be stretched only vertically.
|
||||
///
|
||||
/// The stretching will be applied in order to make the image fit into the box
|
||||
/// specified by [fit]. When [centerSlice] is not null, [fit] defaults to
|
||||
/// [BoxFit.fill], which distorts the destination image size relative to the
|
||||
/// image's original aspect ratio. Values of [BoxFit] which do not distort the
|
||||
/// destination image size will result in [centerSlice] having no effect
|
||||
/// (since the nine regions of the image will be rendered with the same
|
||||
/// scaling, as if it wasn't specified).
|
||||
final Rect centerSlice;
|
||||
|
||||
/// How to paint any portions of the box that would not otherwise be covered
|
||||
/// by the image.
|
||||
final ImageRepeat repeat;
|
||||
|
||||
/// Whether to paint the image in the direction of the [TextDirection].
|
||||
///
|
||||
/// If this is true, then in [TextDirection.ltr] contexts, the image will be
|
||||
/// drawn with its origin in the top left (the "normal" painting direction for
|
||||
/// images); and in [TextDirection.rtl] contexts, the image will be drawn with
|
||||
/// a scaling factor of -1 in the horizontal direction so that the origin is
|
||||
/// in the top right.
|
||||
final bool matchTextDirection;
|
||||
|
||||
/// Creates a [DecorationImagePainterPlus] for this [DecorationImagePlus].
|
||||
///
|
||||
/// The `onChanged` argument must not be null. It will be called whenever the
|
||||
/// image needs to be repainted, e.g. because it is loading incrementally or
|
||||
/// because it is animated.
|
||||
DecorationImagePainterPlus createPainter(VoidCallback onChanged) {
|
||||
assert(onChanged != null);
|
||||
return DecorationImagePainterPlus._(this, onChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is DecorationImagePlus &&
|
||||
other.runtimeType == runtimeType &&
|
||||
image == other.image &&
|
||||
colorFilter == other.colorFilter &&
|
||||
fit == other.fit &&
|
||||
alignment == other.alignment &&
|
||||
centerSlice == other.centerSlice &&
|
||||
repeat == other.repeat &&
|
||||
matchTextDirection == other.matchTextDirection;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(image, colorFilter, fit, alignment,
|
||||
centerSlice, repeat, matchTextDirection);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final List<String> properties = <String>['$image'];
|
||||
if (colorFilter != null) properties.add('$colorFilter');
|
||||
if (fit != null &&
|
||||
!(fit == BoxFit.fill && centerSlice != null) &&
|
||||
!(fit == BoxFit.scaleDown && centerSlice == null)) {
|
||||
properties.add('$fit');
|
||||
}
|
||||
properties.add('$alignment');
|
||||
if (centerSlice != null) properties.add('centerSlice: $centerSlice');
|
||||
if (repeat != ImageRepeat.noRepeat) properties.add('$repeat');
|
||||
if (matchTextDirection) properties.add('match text direction');
|
||||
return '$runtimeType(${properties.join(", ")})';
|
||||
}
|
||||
}
|
||||
|
||||
/// The painter for a [DecorationImagePlus].
|
||||
///
|
||||
/// To obtain a painter, call [DecorationImagePlus.createPainter].
|
||||
///
|
||||
/// To paint, call [paint]. The `onChanged` callback passed to
|
||||
/// [DecorationImagePlus.createPainter] will be called if the image needs to paint
|
||||
/// again (e.g. because it is animated or because it had not yet loaded the
|
||||
/// first time the [paint] method was called).
|
||||
///
|
||||
/// This object should be disposed using the [dispose] method when it is no
|
||||
/// longer needed.
|
||||
class DecorationImagePainterPlus implements DecorationImagePainter {
|
||||
DecorationImagePainterPlus._(this._details, this._onChanged)
|
||||
: assert(_details != null);
|
||||
|
||||
final DecorationImagePlus _details;
|
||||
final VoidCallback _onChanged;
|
||||
|
||||
ImageStream _imageStream;
|
||||
ImageInfo _image;
|
||||
|
||||
/// Draw the image onto the given canvas.
|
||||
///
|
||||
/// The image is drawn at the position and size given by the `rect` argument.
|
||||
///
|
||||
/// The image is clipped to the given `clipPath`, if any.
|
||||
///
|
||||
/// The `configuration` object is used to resolve the image (e.g. to pick
|
||||
/// resolution-specific assets), and to implement the
|
||||
/// [DecorationImagePlus.matchTextDirection] feature.
|
||||
///
|
||||
/// If the image needs to be painted again, e.g. because it is animated or
|
||||
/// because it had not yet been loaded the first time this method was called,
|
||||
/// then the `onChanged` callback passed to [DecorationImagePlus.createPainter]
|
||||
/// will be called.
|
||||
void paint(Canvas canvas, Rect rect, Path clipPath,
|
||||
ImageConfiguration configuration) {
|
||||
assert(canvas != null);
|
||||
assert(rect != null);
|
||||
assert(configuration != null);
|
||||
|
||||
if (_details.matchTextDirection) {
|
||||
assert(() {
|
||||
// We check this first so that the assert will fire immediately, not just
|
||||
// when the image is ready.
|
||||
if (configuration.textDirection == null) {
|
||||
throw FlutterError(
|
||||
'ImageDecoration.matchTextDirection can only be used when a TextDirection is available.\n'
|
||||
'When DecorationImagePainter.paint() was called, there was no text direction provided '
|
||||
'in the ImageConfiguration object to match.\n'
|
||||
'The DecorationImage was:\n'
|
||||
' $_details\n'
|
||||
'The ImageConfiguration was:\n'
|
||||
' $configuration');
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
final ImageStream newImageStream = _details.image.resolve(configuration);
|
||||
if (newImageStream.key != _imageStream?.key) {
|
||||
_imageStream?.removeListener(_imageListener);
|
||||
_imageStream = newImageStream..addListener(_imageListener);
|
||||
}
|
||||
if (_image == null) return;
|
||||
|
||||
if (clipPath != null) {
|
||||
canvas
|
||||
..save()
|
||||
..clipPath(clipPath);
|
||||
}
|
||||
|
||||
_paintImage(
|
||||
canvas: canvas,
|
||||
puzzleWidth: _details.puzzleWidth,
|
||||
puzzleHeight: _details.puzzleHeight,
|
||||
pieceIndex: _details.pieceIndex,
|
||||
rect: rect,
|
||||
image: _image.image,
|
||||
scale: _image.scale,
|
||||
colorFilter: _details.colorFilter,
|
||||
fit: _details.fit,
|
||||
alignment: _details.alignment.resolve(configuration.textDirection),
|
||||
);
|
||||
|
||||
if (clipPath != null) canvas.restore();
|
||||
}
|
||||
|
||||
void _imageListener(ImageInfo value, bool synchronousCall) {
|
||||
if (_image == value) return;
|
||||
_image = value;
|
||||
assert(_onChanged != null);
|
||||
if (!synchronousCall) _onChanged();
|
||||
}
|
||||
|
||||
/// Releases the resources used by this painter.
|
||||
///
|
||||
/// This should be called whenever the painter is no longer needed.
|
||||
///
|
||||
/// After this method has been called, the object is no longer usable.
|
||||
@mustCallSuper
|
||||
void dispose() {
|
||||
_imageStream?.removeListener(_imageListener);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$runtimeType(stream: $_imageStream, image: $_image) for $_details';
|
||||
}
|
||||
}
|
||||
|
||||
void _paintImage(
|
||||
{@required Canvas canvas,
|
||||
@required Rect rect,
|
||||
@required ui.Image image,
|
||||
@required int puzzleWidth,
|
||||
@required int puzzleHeight,
|
||||
@required int pieceIndex,
|
||||
double scale = 1.0,
|
||||
ColorFilter colorFilter,
|
||||
BoxFit fit,
|
||||
Alignment alignment = Alignment.center}) {
|
||||
assert(canvas != null);
|
||||
assert(image != null);
|
||||
assert(alignment != null);
|
||||
|
||||
if (rect.isEmpty) return;
|
||||
final outputSize = rect.size;
|
||||
final inputSize = Size(image.width.toDouble(), image.height.toDouble());
|
||||
fit ??= BoxFit.scaleDown;
|
||||
final FittedSizes fittedSizes =
|
||||
applyBoxFit(fit, inputSize / scale, outputSize);
|
||||
final Size sourceSize = fittedSizes.source * scale;
|
||||
final destinationSize = fittedSizes.destination;
|
||||
final Paint paint = Paint()
|
||||
..isAntiAlias = false
|
||||
..filterQuality = FilterQuality.medium;
|
||||
if (colorFilter != null) paint.colorFilter = colorFilter;
|
||||
final double halfWidthDelta =
|
||||
(outputSize.width - destinationSize.width) / 2.0;
|
||||
final double halfHeightDelta =
|
||||
(outputSize.height - destinationSize.height) / 2.0;
|
||||
final double dx = halfWidthDelta + (alignment.x) * halfWidthDelta;
|
||||
final double dy = halfHeightDelta + alignment.y * halfHeightDelta;
|
||||
final Offset destinationPosition = rect.topLeft.translate(dx, dy);
|
||||
final Rect destinationRect = destinationPosition & destinationSize;
|
||||
final Rect sourceRect =
|
||||
alignment.inscribe(sourceSize, Offset.zero & inputSize);
|
||||
|
||||
final sliceSize =
|
||||
Size(sourceRect.width / puzzleWidth, sourceRect.height / puzzleHeight);
|
||||
|
||||
final col = pieceIndex % puzzleWidth;
|
||||
final row = pieceIndex ~/ puzzleWidth;
|
||||
|
||||
final sliceRect = Rect.fromLTWH(
|
||||
sourceRect.left + col * sliceSize.width,
|
||||
sourceRect.top + row * sliceSize.height,
|
||||
sliceSize.width,
|
||||
sliceSize.height);
|
||||
|
||||
canvas.drawImageRect(image, sliceRect, destinationRect, paint);
|
||||
}
|
||||
106
web/slide_puzzle/lib/src/widgets/material_interior_alt.dart
Normal file
106
web/slide_puzzle/lib/src/widgets/material_interior_alt.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import '../flutter.dart';
|
||||
|
||||
// Copied from
|
||||
// https://github.com/flutter/flutter/blob/f5b02e3c05ed1ab31e890add84fb56e35de2d392/packages/flutter/lib/src/material/material.dart#L593-L715
|
||||
// So I could have animated color!
|
||||
// TODO(kevmoo): file a feature request for this?
|
||||
class MaterialInterior extends ImplicitlyAnimatedWidget {
|
||||
const MaterialInterior({
|
||||
Key key,
|
||||
@required this.child,
|
||||
@required this.shape,
|
||||
@required this.color,
|
||||
Curve curve = Curves.linear,
|
||||
@required Duration duration,
|
||||
}) : assert(child != null),
|
||||
assert(shape != null),
|
||||
assert(color != null),
|
||||
super(key: key, curve: curve, duration: duration);
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// {@macro flutter.widgets.child}
|
||||
final Widget child;
|
||||
|
||||
/// The border of the widget.
|
||||
///
|
||||
/// This border will be painted, and in addition the outer path of the border
|
||||
/// determines the physical shape.
|
||||
final ShapeBorder shape;
|
||||
|
||||
/// The target background color.
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
_MaterialInteriorState createState() => _MaterialInteriorState();
|
||||
}
|
||||
|
||||
class _MaterialInteriorState extends AnimatedWidgetBaseState<MaterialInterior> {
|
||||
ShapeBorderTween _border;
|
||||
ColorTween _color;
|
||||
|
||||
@override
|
||||
void forEachTween(TweenVisitor<dynamic> visitor) {
|
||||
_border = visitor(_border, widget.shape,
|
||||
(value) => ShapeBorderTween(begin: value as ShapeBorder))
|
||||
as ShapeBorderTween;
|
||||
_color = visitor(
|
||||
_color, widget.color, (value) => ColorTween(begin: value as Color))
|
||||
as ColorTween;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shape = _border.evaluate(animation);
|
||||
return PhysicalShape(
|
||||
child: _ShapeBorderPaint(
|
||||
child: widget.child,
|
||||
shape: shape,
|
||||
),
|
||||
clipper: ShapeBorderClipper(
|
||||
shape: shape,
|
||||
textDirection: Directionality.of(context),
|
||||
),
|
||||
color: _color.evaluate(animation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShapeBorderPaint extends StatelessWidget {
|
||||
const _ShapeBorderPaint({
|
||||
@required this.child,
|
||||
@required this.shape,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final ShapeBorder shape;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
child: child,
|
||||
foregroundPainter: _ShapeBorderPainter(shape, Directionality.of(context)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShapeBorderPainter extends CustomPainter {
|
||||
_ShapeBorderPainter(this.border, this.textDirection);
|
||||
|
||||
final ShapeBorder border;
|
||||
final TextDirection textDirection;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
border.paint(canvas, Offset.zero & size, textDirection: textDirection);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_ShapeBorderPainter oldDelegate) {
|
||||
return oldDelegate.border != border;
|
||||
}
|
||||
}
|
||||
471
web/slide_puzzle/pubspec.lock
Normal file
471
web/slide_puzzle/pubspec.lock
Normal file
@@ -0,0 +1,471 @@
|
||||
# Generated by pub
|
||||
# See https://www.dartlang.org/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.36.3"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
bazel_worker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bazel_worker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.20"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
build_modules:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_modules
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
build_web_compilers:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_web_compilers
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
built_value:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.14.11"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.16.0"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.7"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.10.9"
|
||||
flutter_web:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/flutter_web"
|
||||
ref: HEAD
|
||||
resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4"
|
||||
url: "https://github.com/flutter/flutter_web"
|
||||
source: git
|
||||
version: "0.0.0"
|
||||
flutter_web_ui:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/flutter_web_ui"
|
||||
ref: HEAD
|
||||
resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4"
|
||||
url: "https://github.com/flutter/flutter_web"
|
||||
source: git
|
||||
version: "0.0.0"
|
||||
front_end:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: front_end
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.18"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.7"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.14.0+2"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.0+2"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.15.8"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.3"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.1+1"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
kernel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: kernel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.18"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.11.3+2"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.5"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.7"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.6+2"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
package_resolver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_resolver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.10"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.2"
|
||||
pedantic:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protobuf
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.13.11"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
scratch_space:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: scratch_space
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.3+2"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.5"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.10.8"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.5.5"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.3"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.19"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.1+1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.6"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.7+10"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.12"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.15"
|
||||
sdks:
|
||||
dart: ">=2.3.0-dev.0.1 <3.0.0"
|
||||
26
web/slide_puzzle/pubspec.yaml
Normal file
26
web/slide_puzzle/pubspec.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
name: flutter_web.examples.slide_puzzle
|
||||
|
||||
environment:
|
||||
sdk: ">=2.2.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter_web: any
|
||||
flutter_web_ui: any
|
||||
|
||||
dev_dependencies:
|
||||
pedantic: ^1.3.0
|
||||
|
||||
build_runner: any
|
||||
build_web_compilers: any
|
||||
|
||||
# flutter_web packages are not published to pub.dartlang.org
|
||||
# These overrides tell the package tools to get them from GitHub
|
||||
dependency_overrides:
|
||||
flutter_web:
|
||||
git:
|
||||
url: https://github.com/flutter/flutter_web
|
||||
path: packages/flutter_web
|
||||
flutter_web_ui:
|
||||
git:
|
||||
url: https://github.com/flutter/flutter_web
|
||||
path: packages/flutter_web_ui
|
||||
18
web/slide_puzzle/web/assets/FontManifest.json
Normal file
18
web/slide_puzzle/web/assets/FontManifest.json
Normal file
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"family": "MaterialIcons",
|
||||
"fonts": [
|
||||
{
|
||||
"asset": "https://fonts.gstatic.com/s/materialicons/v47/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"family": "Plaster",
|
||||
"fonts": [
|
||||
{
|
||||
"asset": "plaster.woff2"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
BIN
web/slide_puzzle/web/assets/plaster.woff2
Normal file
BIN
web/slide_puzzle/web/assets/plaster.woff2
Normal file
Binary file not shown.
BIN
web/slide_puzzle/web/assets/seattle.jpg
Normal file
BIN
web/slide_puzzle/web/assets/seattle.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 382 KiB |
10
web/slide_puzzle/web/index.html
Normal file
10
web/slide_puzzle/web/index.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
<script defer src="main.dart.js" type="application/javascript"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
11
web/slide_puzzle/web/main.dart
Normal file
11
web/slide_puzzle/web/main.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright 2019 The Chromium Authors. 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_web_ui/ui.dart' as ui;
|
||||
import 'package:flutter_web.examples.slide_puzzle/main.dart' as app;
|
||||
|
||||
void main() async {
|
||||
await ui.webOnlyInitializePlatform();
|
||||
app.main();
|
||||
}
|
||||
BIN
web/slide_puzzle/web/preview.png
Normal file
BIN
web/slide_puzzle/web/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Reference in New Issue
Block a user