feat(call): aufgeblasene Formen statt runder Bubbles (Bernds Referenzbild)
- Neues Layout lib/features/call_ui/inflated_layout.dart: gewichtetes Voronoi-/Potenzdiagramm im Anrufbereich, Zellen fuellen das Fenster bis an die Raender, an Nachbarn plattgedrueckt, weich gerundete Ecken; Lloyd-Relaxation + sanfter Zug ins Raster (aufgeraeumt), Flaechen per Gewicht nachgefuehrt (Sprecher 1,7-fach), Neue pumpen sich auf, Gehende schrumpfen weg, je mehr Personen desto kleiner - Darstellung nur als Kontur mit weichem Innenleuchten, Profilbild mit Kontur + Name in der Mitte, Sprecher gruen pulsierend, Kamera-Bild fuellt die Form - Tests: test/inflated_layout_test.dart (7), Vorschau-/Animationsbilder integration_test/call_bubbles_demo_test.dart; alte Kugel-Physik entfernt - Version 0.1.43 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d4473c7d64
commit
ab84701e73
@@ -1,94 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pyramid/features/call_ui/bubble_physics.dart';
|
||||
|
||||
/// Lässt die Simulation [seconds] lang laufen (60 Schritte pro Sekunde).
|
||||
bool _run(BubbleSim sim, double seconds) {
|
||||
var moving = true;
|
||||
for (var i = 0; i < (seconds * 60).round(); i++) {
|
||||
moving = sim.step(1 / 60);
|
||||
}
|
||||
return moving;
|
||||
}
|
||||
|
||||
void main() {
|
||||
const phone = Size(390, 620);
|
||||
const desktop = Size(1200, 700);
|
||||
|
||||
test('zwei Bubbles drücken sich leicht aneinander und kommen zur Ruhe', () {
|
||||
final sim = BubbleSim()..sync(['a', 'b'], {}, desktop);
|
||||
final moving = _run(sim, 6);
|
||||
expect(moving, isFalse, reason: 'Simulation kommt nicht zur Ruhe');
|
||||
|
||||
final a = sim.bodies['a']!, b = sim.bodies['b']!;
|
||||
final dist = (a.pos - b.pos).distance;
|
||||
// Sie berühren sich (überlappen leicht) statt frei zu schweben …
|
||||
expect(dist, lessThan(a.r + b.r));
|
||||
// … liegen aber nicht ineinander.
|
||||
expect(dist, greaterThan((a.r + b.r) * 0.8));
|
||||
// Querformat: nebeneinander statt übereinander
|
||||
expect((a.pos.dy - b.pos.dy).abs(), lessThan((a.pos.dx - b.pos.dx).abs()));
|
||||
});
|
||||
|
||||
test('wer spricht, wird größer', () {
|
||||
final sim = BubbleSim()..sync(['a', 'b', 'c'], {}, phone);
|
||||
_run(sim, 4);
|
||||
final before = sim.bodies['b']!.r;
|
||||
sim.sync(['a', 'b', 'c'], {'b'}, phone);
|
||||
_run(sim, 4);
|
||||
final b = sim.bodies['b']!.r;
|
||||
expect(b, greaterThan(before * 1.1));
|
||||
expect(b, greaterThan(sim.bodies['a']!.r * 1.1));
|
||||
});
|
||||
|
||||
test('viele Teilnehmer bleiben im Bild (Handy hochkant)', () {
|
||||
final ids = [for (var i = 0; i < 8; i++) 'p$i'];
|
||||
final sim = BubbleSim()..sync(ids, {}, phone);
|
||||
_run(sim, 8);
|
||||
for (final b in sim.bodies.values) {
|
||||
expect(b.pos.dx - b.r, greaterThan(-2), reason: '${b.id} links raus');
|
||||
expect(b.pos.dx + b.r, lessThan(phone.width + 2), reason: '${b.id} rechts raus');
|
||||
expect(b.pos.dy - b.r, greaterThan(-2), reason: '${b.id} oben raus');
|
||||
expect(b.pos.dy + b.r, lessThan(phone.height + 2), reason: '${b.id} unten raus');
|
||||
}
|
||||
});
|
||||
|
||||
test('Gehende schrumpfen weg und verschwinden', () {
|
||||
final sim = BubbleSim()..sync(['a', 'b'], {}, phone);
|
||||
_run(sim, 3);
|
||||
sim.sync(['a'], {}, phone);
|
||||
_run(sim, 3);
|
||||
expect(sim.bodies.keys, ['a']);
|
||||
});
|
||||
|
||||
test('Berührungsfläche ist abgeflacht, freier Rand bleibt rund', () {
|
||||
final sim = BubbleSim()..sync(['a', 'b'], {}, desktop);
|
||||
_run(sim, 6);
|
||||
final a = sim.bodies['a']!, b = sim.bodies['b']!;
|
||||
final bounds = sim.clipPath('a').getBounds();
|
||||
final toB = (b.pos - a.pos);
|
||||
// In Richtung des Nachbarn endet die Bubble vor ihrem Kreisrand …
|
||||
if (toB.dx > 0) {
|
||||
expect(bounds.right, lessThan(a.pos.dx + a.r - 1));
|
||||
// … auf der freien Seite reicht sie bis zum Kreisrand.
|
||||
expect(bounds.left, closeTo(a.pos.dx - a.r, 1));
|
||||
} else {
|
||||
expect(bounds.left, greaterThan(a.pos.dx - a.r + 1));
|
||||
expect(bounds.right, closeTo(a.pos.dx + a.r, 1));
|
||||
}
|
||||
// Mittelpunkt liegt im Umriss, der Nachbar-Mittelpunkt nicht.
|
||||
expect(sim.clipPath('a').contains(a.pos), isTrue);
|
||||
expect(sim.clipPath('a').contains(b.pos), isFalse);
|
||||
});
|
||||
|
||||
test('einzelne Bubble: rund, mittig', () {
|
||||
final sim = BubbleSim()..sync(['solo'], {}, phone);
|
||||
_run(sim, 4);
|
||||
final s = sim.bodies['solo']!;
|
||||
expect(s.pos.dx, closeTo(phone.width / 2, 2));
|
||||
expect(s.pos.dy, closeTo(phone.height / 2, 2));
|
||||
final bounds = sim.clipPath('solo').getBounds();
|
||||
expect(bounds.width, closeTo(s.r * 2, 1));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pyramid/features/call_ui/inflated_layout.dart';
|
||||
|
||||
/// [seconds] lang simulieren (60 Schritte pro Sekunde).
|
||||
bool _run(InflatedLayout l, double seconds) {
|
||||
var moving = true;
|
||||
for (var i = 0; i < (seconds * 60).round(); i++) {
|
||||
moving = l.step(1 / 60);
|
||||
}
|
||||
return moving;
|
||||
}
|
||||
|
||||
List<String> _ids(int n) => [for (var i = 0; i < n; i++) 'p$i'];
|
||||
|
||||
void main() {
|
||||
const phone = Size(390, 620);
|
||||
const desktop = Size(1200, 700);
|
||||
|
||||
test('eine Person füllt den ganzen Bereich bis an die Ränder', () {
|
||||
final l = InflatedLayout()..sync(['solo'], {}, phone);
|
||||
_run(l, 1);
|
||||
final r = l.cellBounds('solo');
|
||||
expect(r.left, closeTo(l.margin, 0.5));
|
||||
expect(r.top, closeTo(l.margin, 0.5));
|
||||
expect(r.right, closeTo(phone.width - l.margin, 0.5));
|
||||
expect(r.bottom, closeTo(phone.height - l.margin, 0.5));
|
||||
// Gerundete Ecken: der Umriss enthält die Ecke des Rechtecks nicht mehr.
|
||||
final path = l.outline('solo');
|
||||
expect(path.contains(r.topLeft + const Offset(2, 2)), isFalse);
|
||||
expect(path.contains(r.center), isTrue);
|
||||
});
|
||||
|
||||
test('vier Personen: gleich große Zellen, zusammen fast die ganze Fläche', () {
|
||||
final l = InflatedLayout()..sync(_ids(4), {}, desktop);
|
||||
final moving = _run(l, 6);
|
||||
expect(moving, isFalse, reason: 'kommt nicht zur Ruhe');
|
||||
final areas = l.cells.values.map((c) => c.area).toList();
|
||||
final avg = areas.reduce((a, b) => a + b) / areas.length;
|
||||
for (final a in areas) {
|
||||
expect(a, closeTo(avg, avg * 0.08));
|
||||
}
|
||||
final total = areas.reduce((a, b) => a + b);
|
||||
expect(total, greaterThan(l.bounds.width * l.bounds.height * 0.9));
|
||||
});
|
||||
|
||||
test('wer spricht, bekommt deutlich mehr Fläche', () {
|
||||
final l = InflatedLayout()..sync(_ids(3), {}, phone);
|
||||
_run(l, 4);
|
||||
l.sync(_ids(3), {'p1'}, phone);
|
||||
_run(l, 5);
|
||||
final speaker = l.cells['p1']!.area;
|
||||
final other = l.cells['p0']!.area;
|
||||
expect(speaker / other,
|
||||
closeTo(InflatedLayout.speakingShare, InflatedLayout.speakingShare * 0.15));
|
||||
});
|
||||
|
||||
test('je mehr Personen, desto kleiner die Zellen', () {
|
||||
double avgArea(int n) {
|
||||
final l = InflatedLayout()..sync(_ids(n), {}, phone);
|
||||
_run(l, 5);
|
||||
return l.cells.values.fold<double>(0, (s, c) => s + c.area) / n;
|
||||
}
|
||||
|
||||
final a2 = avgArea(2), a5 = avgArea(5), a9 = avgArea(9);
|
||||
expect(a5, lessThan(a2));
|
||||
expect(a9, lessThan(a5));
|
||||
});
|
||||
|
||||
test('Neue pumpen sich auf, Gehende verschwinden', () {
|
||||
final l = InflatedLayout()..sync(_ids(2), {}, phone);
|
||||
_run(l, 3);
|
||||
l.sync(_ids(3), {}, phone);
|
||||
expect(l.cells['p2']!.area, lessThan(l.cells['p0']!.area));
|
||||
_run(l, 5);
|
||||
expect(l.cells['p2']!.area, closeTo(l.cells['p0']!.area, l.cells['p0']!.area * 0.1));
|
||||
l.sync(['p0', 'p2'], {}, phone);
|
||||
_run(l, 3);
|
||||
expect(l.cells.keys.toSet(), {'p0', 'p2'});
|
||||
});
|
||||
|
||||
test('Zellen überlappen nicht und jeder Mittelpunkt liegt in seiner Zelle', () {
|
||||
final l = InflatedLayout()..sync(_ids(7), {'p3'}, desktop);
|
||||
_run(l, 6);
|
||||
final paths = {for (final id in l.cells.keys) id: l.outline(id)};
|
||||
for (final c in l.cells.values) {
|
||||
expect(paths[c.id]!.contains(c.centroid), isTrue, reason: '${c.id} Schwerpunkt');
|
||||
for (final other in l.cells.values) {
|
||||
if (other.id == c.id) continue;
|
||||
expect(paths[other.id]!.contains(c.centroid), isFalse,
|
||||
reason: '${c.id} liegt in ${other.id}');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('Drehen (hochkant → quer): zwei Personen nebeneinander', () {
|
||||
final l = InflatedLayout()..sync(_ids(2), {}, phone);
|
||||
_run(l, 3);
|
||||
final a = l.cells['p0']!.centroid, b = l.cells['p1']!.centroid;
|
||||
expect((a.dy - b.dy).abs(), greaterThan((a.dx - b.dx).abs()), reason: 'hochkant: übereinander');
|
||||
l.sync(_ids(2), {}, desktop);
|
||||
_run(l, 3);
|
||||
final c = l.cells['p0']!.centroid, d = l.cells['p1']!.centroid;
|
||||
expect((c.dx - d.dx).abs(), greaterThan((c.dy - d.dy).abs()), reason: 'quer: nebeneinander');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user