change slide_puzzle to a submodule

pull/441/head
John Ryan 5 years ago
parent baa1f976b2
commit fcf6c06152

3
.gitmodules vendored

@ -0,0 +1,3 @@
[submodule "web/slide_puzzle"]
path = web/slide_puzzle
url = git@github.com:kevmoo/slide_puzzle.git

@ -0,0 +1 @@
Subproject commit 5ef5526acb58f9ffb6f3fb22118cbd825613dc73

@ -1,4 +0,0 @@
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).

@ -1,94 +0,0 @@
Copyright (c) 2011 by Sorkin Type Co (www.sorkintype.com),
with Reserved Font Name "Plaster".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 KiB

@ -1,33 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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);
@override
PuzzleHomeState createState() =>
PuzzleHomeState(PuzzleAnimator(_columns, _rows));
}

@ -1,13 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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/foundation.dart';
import 'core/puzzle_proxy.dart';
abstract class AppState {
PuzzleProxy get puzzle;
Listenable get animationNotifier;
}

@ -1,113 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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;
}

@ -1,12 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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);
}

@ -1,275 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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.');
}
}

@ -1,177 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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';
import 'puzzle_proxy.dart';
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;
int get incorrectTiles => _puzzle.incorrectTiles;
int get clickCount => _clickCount;
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.random);
}
@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());
}
}

@ -1,25 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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;
enum PuzzleEvent { click, random, reset, noop }
abstract class PuzzleProxy {
int get width;
int get height;
int get length;
bool get solved;
void clickOrShake(int tileValue);
int get tileCount;
Point<double> location(int index);
bool isCorrectPosition(int value);
}

@ -1,63 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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;
}
}

@ -1,188 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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;
}
}

@ -1,58 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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;
}

@ -1,6 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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/material.dart';
export 'package:flutter/scheduler.dart' show Ticker;

@ -1,17 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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/foundation.dart';
abstract class PuzzleControls implements Listenable {
void reset();
int get clickCount;
int get incorrectTiles;
bool get autoPlay;
void Function(bool newValue) get setAutoPlayFunction;
}

@ -1,43 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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_proxy.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);
}

@ -1,285 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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 'package:provider/provider.dart';
import 'app_state.dart';
import 'core/puzzle_animator.dart';
import 'core/puzzle_proxy.dart';
import 'flutter.dart';
import 'puzzle_controls.dart';
import 'puzzle_flow_delegate.dart';
import 'shared_theme.dart';
import 'themes.dart';
import 'value_tab_controller.dart';
class _PuzzleControls extends ChangeNotifier implements PuzzleControls {
final PuzzleHomeState _parent;
_PuzzleControls(this._parent);
@override
bool get autoPlay => _parent._autoPlay;
void _notify() => notifyListeners();
@override
void Function(bool newValue) get setAutoPlayFunction {
if (_parent.puzzle.solved) {
return null;
}
return _parent._setAutoPlay;
}
@override
int get clickCount => _parent.puzzle.clickCount;
@override
int get incorrectTiles => _parent.puzzle.incorrectTiles;
@override
void reset() => _parent.puzzle.reset();
}
class PuzzleHomeState extends State
with SingleTickerProviderStateMixin, AppState {
@override
final PuzzleAnimator puzzle;
@override
final _AnimationNotifier animationNotifier = _AnimationNotifier();
Duration _tickerTimeSinceLastEvent = Duration.zero;
Ticker _ticker;
Duration _lastElapsed;
StreamSubscription _puzzleEventSubscription;
bool _autoPlay = false;
_PuzzleControls _autoPlayListenable;
PuzzleHomeState(this.puzzle) {
_puzzleEventSubscription = puzzle.onEvent.listen(_onPuzzleEvent);
}
@override
void initState() {
super.initState();
_autoPlayListenable = _PuzzleControls(this);
_ticker ??= createTicker(_onTick);
_ensureTicking();
}
void _setAutoPlay(bool newValue) {
if (newValue != _autoPlay) {
setState(() {
// Only allow enabling autoPlay if the puzzle is not solved
_autoPlayListenable._notify();
_autoPlay = newValue && !puzzle.solved;
if (_autoPlay) {
_ensureTicking();
}
});
}
}
@override
Widget build(BuildContext context) => MultiProvider(
providers: [
Provider<AppState>.value(value: this),
ListenableProvider<PuzzleControls>.value(
listenable: _autoPlayListenable,
)
],
child: Material(
child: Stack(
children: <Widget>[
const SizedBox.expand(
child: FittedBox(
fit: BoxFit.cover,
child: Image(
image: AssetImage('asset/seattle.jpg'),
),
),
),
const LayoutBuilder(builder: _doBuild),
],
),
),
);
@override
void dispose() {
animationNotifier.dispose();
_ticker?.dispose();
_autoPlayListenable?.dispose();
_puzzleEventSubscription.cancel();
super.dispose();
}
void _onPuzzleEvent(PuzzleEvent e) {
_autoPlayListenable._notify();
if (e != PuzzleEvent.random) {
_setAutoPlay(false);
}
_tickerTimeSinceLastEvent = Duration.zero;
_ensureTicking();
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 {
void animate() {
notifyListeners();
}
}
const _maxFrameDuration = Duration(milliseconds: 34);
Widget _updateConstraints(
BoxConstraints constraints, Widget Function(bool small) builder) {
const _smallWidth = 580;
final constraintWidth =
constraints.hasBoundedWidth ? constraints.maxWidth : 1000.0;
final constraintHeight =
constraints.hasBoundedHeight ? constraints.maxHeight : 1000.0;
return builder(constraintWidth < _smallWidth || constraintHeight < 690);
}
Widget _doBuild(BuildContext _, BoxConstraints constraints) =>
_updateConstraints(constraints, _doBuildCore);
Widget _doBuildCore(bool small) => ValueTabController<SharedTheme>(
values: themes,
child: Consumer<SharedTheme>(
builder: (_, theme, __) => AnimatedContainer(
duration: puzzleAnimationDuration,
color: theme.puzzleThemeBackground,
child: Center(
child: theme.styledWrapper(
small,
SizedBox(
width: 580,
child: Consumer<AppState>(
builder: (context, appState, _) => 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: ValueTabController.of(context),
labelPadding: const EdgeInsets.fromLTRB(0, 20, 0, 12),
labelColor: theme.puzzleAccentColor,
indicatorColor: theme.puzzleAccentColor,
indicatorWeight: 1.5,
unselectedLabelColor: Colors.black.withOpacity(0.6),
tabs: themes
.map((st) => Text(
st.name.toUpperCase(),
style: const TextStyle(
letterSpacing: 0.5,
),
))
.toList(),
),
),
Flexible(
child: Container(
padding: const EdgeInsets.all(10),
child: Flow(
delegate: PuzzleFlowDelegate(
small ? const Size(90, 90) : const Size(140, 140),
appState.puzzle,
appState.animationNotifier,
),
children: List<Widget>.generate(
appState.puzzle.length,
(i) => theme.tileButtonCore(
i, appState.puzzle, small),
),
),
),
),
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: Consumer<PuzzleControls>(
builder: (_, controls, __) =>
Row(children: theme.bottomControls(controls)),
),
)
],
),
),
),
),
),
),
),
);

@ -1,116 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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_proxy.dart';
import 'flutter.dart';
import 'puzzle_controls.dart';
import 'widgets/material_interior_alt.dart';
final puzzleAnimationDuration = kThemeAnimationDuration * 3;
abstract class SharedTheme {
const SharedTheme();
String get name;
Color get puzzleThemeBackground;
RoundedRectangleBorder puzzleBorder(bool small);
Color get puzzleBackgroundColor;
Color get puzzleAccentColor;
EdgeInsetsGeometry tilePadding(PuzzleProxy puzzle) => const EdgeInsets.all(6);
Widget tileButton(int i, PuzzleProxy puzzle, bool small);
Ink createInk(
Widget child, {
DecorationImage image,
EdgeInsetsGeometry padding,
}) =>
Ink(
padding: padding,
decoration: BoxDecoration(
image: image,
),
child: child,
);
Widget createButton(
PuzzleProxy puzzle,
bool small,
int tileValue,
Widget content, {
Color color,
RoundedRectangleBorder shape,
}) =>
AnimatedContainer(
duration: puzzleAnimationDuration,
padding: tilePadding(puzzle),
child: RaisedButton(
elevation: 4,
clipBehavior: Clip.hardEdge,
animationDuration: puzzleAnimationDuration,
onPressed: () => puzzle.clickOrShake(tileValue),
shape: shape ?? puzzleBorder(small),
padding: const EdgeInsets.symmetric(),
child: content,
color: color,
),
);
// Thought about using AnimatedContainer here, but it causes some weird
// resizing behavior
Widget styledWrapper(bool small, Widget child) => MaterialInterior(
duration: puzzleAnimationDuration,
shape: puzzleBorder(small),
color: puzzleBackgroundColor,
child: child,
);
TextStyle get _infoStyle => TextStyle(
color: puzzleAccentColor,
fontWeight: FontWeight.bold,
);
List<Widget> bottomControls(PuzzleControls controls) => <Widget>[
IconButton(
onPressed: controls.reset,
icon: Icon(Icons.refresh, color: puzzleAccentColor),
),
Checkbox(
value: controls.autoPlay,
onChanged: controls.setAutoPlayFunction,
activeColor: puzzleAccentColor,
),
Expanded(
child: Container(),
),
Text(
controls.clickCount.toString(),
textAlign: TextAlign.right,
style: _infoStyle,
),
const Text(' Moves'),
SizedBox(
width: 28,
child: Text(
controls.incorrectTiles.toString(),
textAlign: TextAlign.right,
style: _infoStyle,
),
),
const Text(' Tiles left ')
];
Widget tileButtonCore(int i, PuzzleProxy puzzle, bool small) {
if (i == puzzle.tileCount && !puzzle.solved) {
return const Center();
}
return tileButton(i, puzzle, small);
}
}

@ -1,78 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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_proxy.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';
const ThemePlaster();
@override
Color get puzzleThemeBackground => _chocolate;
@override
Color get puzzleBackgroundColor => _yellowIsh;
@override
Color get puzzleAccentColor => _orangeIsh;
@override
RoundedRectangleBorder puzzleBorder(bool small) => 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, PuzzleProxy puzzle, bool small) {
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(
puzzle,
small,
i,
content,
color: primary ? _orangeIsh : _yellowIsh,
shape: RoundedRectangleBorder(
side: BorderSide(color: primary ? _chocolate : _orangeIsh, width: 5),
borderRadius: BorderRadius.circular(5),
),
);
}
}

@ -1,75 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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_proxy.dart';
import 'flutter.dart';
import 'shared_theme.dart';
import 'widgets/decoration_image_plus.dart';
class ThemeSeattle extends SharedTheme {
@override
String get name => 'Seattle';
const ThemeSeattle();
@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 puzzleBorder(bool small) =>
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(1),
),
);
@override
EdgeInsetsGeometry tilePadding(PuzzleProxy puzzle) =>
puzzle.solved ? const EdgeInsets.all(1) : const EdgeInsets.all(4);
@override
Widget tileButton(int i, PuzzleProxy puzzle, bool small) {
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('asset/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(puzzle, small, i, content);
}
}

@ -1,71 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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_proxy.dart';
import 'flutter.dart';
import 'shared_theme.dart';
const _accentBlue = Color(0xff000579e);
class ThemeSimple extends SharedTheme {
@override
String get name => 'Simple';
const ThemeSimple();
@override
Color get puzzleThemeBackground => Colors.white;
@override
Color get puzzleBackgroundColor => Colors.white70;
@override
Color get puzzleAccentColor => _accentBlue;
@override
RoundedRectangleBorder puzzleBorder(bool small) =>
const RoundedRectangleBorder(
side: BorderSide(color: Colors.black26, width: 1),
borderRadius: BorderRadius.all(
Radius.circular(4),
),
);
@override
Widget tileButton(int i, PuzzleProxy puzzle, bool small) {
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(
puzzle,
small,
i,
content,
color: const Color.fromARGB(255, 13, 87, 155),
);
}
}

@ -1,13 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'theme_plaster.dart';
import 'theme_seattle.dart';
import 'theme_simple.dart';
const themes = [
ThemeSimple(),
ThemeSeattle(),
ThemePlaster(),
];

@ -1,92 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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 'package:provider/provider.dart';
class ValueTabController<T> extends StatefulWidget {
/// Creates a default tab controller for the given [child] widget.
const ValueTabController({
Key key,
@required this.child,
@required this.values,
}) : super(key: key);
/// The widget below this widget in the tree.
///
/// Typically a [Scaffold] whose [AppBar] includes a [TabBar].
///
/// {@macro flutter.widgets.child}
final Widget child;
final List<T> values;
/// The closest instance of this class that encloses the given context.
///
/// Typical usage:
///
/// ```dart
/// TabController controller = DefaultTabBarController.of(context);
/// ```
static TabController of(BuildContext context) {
final scope = context.inheritFromWidgetOfExactType(_ValueTabControllerScope)
as _ValueTabControllerScope;
return scope?.controller;
}
@override
_ValueTabControllerState<T> createState() => _ValueTabControllerState<T>();
}
class _ValueTabControllerState<T> extends State<ValueTabController<T>>
with SingleTickerProviderStateMixin {
final _notifier = ValueNotifier<T>(null);
TabController _controller;
@override
void initState() {
super.initState();
_controller = TabController(
vsync: this,
length: widget.values.length,
initialIndex: 0,
);
_notifier.value = widget.values.first;
_controller.addListener(() {
_notifier.value = widget.values[_controller.index];
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => _ValueTabControllerScope(
controller: _controller,
enabled: TickerMode.of(context),
child: ValueListenableProvider.value(
valueListenable: _notifier,
child: widget.child,
),
);
}
class _ValueTabControllerScope extends InheritedWidget {
const _ValueTabControllerScope(
{Key key, this.controller, this.enabled, Widget child})
: super(key: key, child: child);
final TabController controller;
final bool enabled;
@override
bool updateShouldNotify(_ValueTabControllerScope old) =>
enabled != old.enabled || controller != old.controller;
}

@ -1,334 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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 'dart:developer' as developer;
import 'dart:ui' 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>[];
properties.add('$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(", ")})';
}
@override
ImageErrorListener get onError => (error, stackTrace) {
developer.log(
'Failed to load image.\n'
'$error\n'
'$stackTrace',
name: 'slide_puzzle.decoration_image_plus');
};
}
/// 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) {
final listener = ImageStreamListener(_imageListener);
_imageStream?.removeListener(listener);
_imageStream = newImageStream;
_imageStream.addListener(listener);
}
if (_image == null) return;
if (clipPath != null) {
canvas.save();
canvas.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(ImageStreamListener(_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);
}

@ -1,106 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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;
}
}

@ -1,426 +0,0 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.39.8"
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.13"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.12"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
coverage:
dependency: transitive
description:
name: coverage
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.9"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.0+3"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.1"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.4"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.12"
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.4"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.1+1"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.4"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.6"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.6+3"
multi_server_socket:
dependency: transitive
description:
name: multi_server_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
node_interop:
dependency: transitive
description:
name: node_interop
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
node_io:
dependency: transitive
description:
name: node_io
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.8"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.4"
pedantic:
dependency: "direct dev"
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.0"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
provider:
dependency: "direct dev"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.5"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
shelf_static:
dependency: transitive
description:
name: shelf_static
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.8"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.3"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
source_maps:
dependency: transitive
description:
name: source_maps
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.9"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
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"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
test:
dependency: "direct dev"
description:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.3"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.15"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.4"
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"
vm_service:
dependency: transitive
description:
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.2"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7+15"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.3"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.6.1"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
sdks:
dart: ">=2.7.0 <3.0.0"

@ -1,27 +0,0 @@
name: slide_puzzle
version: 1.0.0
environment:
sdk: ">=2.0.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
pedantic: ^1.3.0
provider: ^2.0.0
test: ^1.3.4
flutter:
uses-material-design: true
assets:
- asset/
fonts:
- family: Plaster
fonts:
- asset: asset/fonts/plaster/Plaster-Regular.ttf

@ -1,10 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>slide_puzzle</title>
</head>
<body>
<script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
Loading…
Cancel
Save