From 3d6315fe1e0a47efb8fa76ee52f2204751e923ae Mon Sep 17 00:00:00 2001 From: thepra Date: Sat, 17 Oct 2026 01:49:41 +0200 Subject: [PATCH] "me-web?" 0.14.0: the first three cards, drawn again - The three pictures at the top are new: a small planet at dusk under twinkling stars, with town lights on its night side, drifting clouds, smoke from the chimney and trees that sway. - The house and the block of flats now have lit walls, a tiled roof, glowing windows and soft shadows. - The photo now flies like a little print and the visitor is a letter, because a letter needs an address. Each flies along its route with a glowing trail. At the house the door lights up green; at the block of flats the letter stops at the main door while the windows light up one by one, and a question mark appears. - The path from your device to the internet glows in each step's colour. While a step is checked, an arc circles it and small lights travel along the wires, and a ring spreads out when its answer arrives. - In right-to-left languages the small picture badge stays in its corner instead of covering the drawing. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 11 +- Directory.Build.props | 2 +- src/MeWeb.App/Resources/Art.axaml | 275 ++++++++++++- src/MeWeb.App/Resources/Colors.axaml | 71 ++++ src/MeWeb.App/Resources/Styles.axaml | 522 +++++++++++++++++++----- src/MeWeb.App/Views/IntroScene.axaml | 167 +++++--- src/MeWeb.App/Views/IntroScene.axaml.cs | 454 +++++++++++++++------ src/MeWeb.App/Views/MainView.axaml | 16 +- 8 files changed, 1200 insertions(+), 318 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 90f7067..68258ac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,7 +62,7 @@ The Android release key: alias `meweb`, certificate SHA-256 |---|---| | `src/MeWeb.Shared` | `Endpoints.cs` (routes and hosts), wire contracts, `MeWebJsonContext`, generated `BuildInfo` | | `src/MeWeb.Core` | the checks, no UI: `Net/` (address kinds, local network, TTL route, STUN, the probe's `Answerer`, DNS, speed, the device), `Router/` (UPnP IGD, NAT-PMP, PCP; `PortMapper` opens and removes mappings, `Leftovers` remembers them across a crash), `Mail/` (port 25, reverse name, blocklists through the user's resolver), `Knowledge/` (provider map and profiles), `History/` (checks, shared submissions), `Checks/` (`CheckRunner`, `Verdicts`, `Reach`, `Uses`, `ServerProbe`, `SharePayload`) | -| `src/MeWeb.App` | Avalonia views and view models, `Services/Localizer.cs` (+ `{s:Tr Key}`), `Strings/.json`, `Resources/Colors.axaml`; `Resources/Icons.axaml` (Lucide, 24 units, for cards and lists) and `Resources/Art.axaml` (the path's drawings, 48 units: `ArtBase`, up to three `Beat`s that pulse in turn while the step is checked and take its verdict colour, and a `Spin` for the globe). The path is a vertical zig-zag of `PathNodeViewModel`s, and the same nodes carry the result cards (`Cards`, regrouped by `MainViewModel.Home`), so a step in the path and its group of cards are one object | +| `src/MeWeb.App` | Avalonia views and view models, `Services/Localizer.cs` (+ `{s:Tr Key}`), `Strings/.json`, `Resources/Colors.axaml`; `Resources/Icons.axaml` (Lucide, 24 units, for cards and lists) and `Resources/Art.axaml` (the path's drawings, 48 units: `ArtBase`, up to three `Beat`s that pulse in turn while the step is checked and take its verdict colour, and a `Spin` for the globe; then the intro planet's still pieces as `DrawingImage`s with a fixed `Viewbox`: `ArtHouse`, `ArtBlock`, `ArtPine`, `ArtOak`, `ArtPhoto`, `ArtLetter`, `ArtServer`). The path is a vertical zig-zag of `PathNodeViewModel`s, and the same nodes carry the result cards (`Cards`, regrouped by `MainViewModel.Home`), so a step in the path and its group of cards are one object | | `src/MeWeb.Desktop` | desktop head (`meweb` executable), Windows manifest | | `src/MeWeb.Android` | Android head (`software.arasaka.meweb`), resources, manifest | | `src/MeWeb.Server` | `meweb.arasaka.software`: `/api/info`, `/api/ip`, the download page `/` and `/privacy` (en, it), `/download/{platform}` from the latest Gitea release, the probe (`/api/probe/ticket`, `/api/probe/{id}`) and shared findings (`/api/share`, files under `/var/lib/meweb/shares`, swept after `KeepDays`) | @@ -205,7 +205,12 @@ The Android release key: alias `meweb`, certificate SHA-256 `Views/IntroScene` is one small planet in three `Step`s: 0, a phone far away sends a photo into the house and on to the parents' tablet (heart); 1, a house with its own number and a small always-on computer in a callout lets a visitor in (green door, tick); 2, that house next to a block of flats with one number for all, where the visitor - stops at the main door (question mark). The third card asks whether the reader's home has an address of its own and + stops at the main door (question mark). The visitor is a letter, because a letter needs an address; it and the photo + fly along the dotted routes with a comet trail (a gradient stroke whose ends are set each frame in + `RelativeUnit.Absolute`), and the world around them keeps moving (stars, town lights on the night side, clouds, + chimney smoke, swaying trees). The still drawings are `DrawingImage`s in `Art.axaml`; only what moves is a control, + and it moves through its `RenderTransform` (scale, rotate, translate), never `Canvas.SetLeft`, so a frame does not + lay the canvas out again. The third card asks whether the reader's home has an address of its own and offers two answers: "Yes, let's find out" runs the check, "Not for me" shows `Goodbye` for two seconds and closes the app (`IPlatform.Quit`, the desktop lifetime's `Shutdown`). The six uses follow as chips, the QR codes wait folded at the bottom. @@ -216,6 +221,8 @@ The Android release key: alias `meweb`, certificate SHA-256 is the frame loop the scenes share; `Resting` parks a scene that is folded or off screen, so it starts again from the beginning when it comes back. The screenshot tool calls `Hold()` to draw each at its `Poster` moment, because the headless clock is real time. +- **A `Button` clips its content to its own bounds.** The path's node buttons (`Button.hop`) turn `ClipToBounds` off + on the button and its `ContentPresenter`, or the verdict aura and the running pulse are cut into a 90-pixel square. - **Nothing that is being checked is left empty.** From the moment a check starts, every use has a card with its title, a "Checking" chip, two skeleton lines and a sweeping glint (`UseViewModel` with no finding), and every running check card shows the same; the verdicts replace them as they arrive. While a check runs, the share card sits diff --git a/Directory.Build.props b/Directory.Build.props index d9fd02b..c840d2b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.13.0 + 0.14.0 “me-web?” Arasaka di Hvozdikov Yevheniy © 2026 Arasaka di Hvozdikov Yevheniy diff --git a/src/MeWeb.App/Resources/Art.axaml b/src/MeWeb.App/Resources/Art.axaml index d61b5b5..1bc571c 100644 --- a/src/MeWeb.App/Resources/Art.axaml +++ b/src/MeWeb.App/Resources/Art.axaml @@ -33,24 +33,271 @@ M 6 24 A 18 18 0 1 0 42 24 A 18 18 0 1 0 6 24 Z M 6 24 H 42 M 8.4 15 H 39.6 M 8.4 33 H 39.6 M 24 6 A 9 18 0 0 0 24 42 A 9 18 0 0 0 24 6 Z - M 4 72 L 4 40 L 28 18 L 52 40 L 52 72 Z - M 52 40 L 76 30 L 76 62 L 52 72 Z - M 28 18 L 52 8 L 76 30 L 52 40 Z - M 2 41.5 L 28 17 L 54 41.5 - M 21 72 V 57 A 7 7 0 0 1 35 57 V 72 Z - M 58 45 L 70 40 L 70 51 L 58 56 Z - M 64 42.5 V 53.5 M 58 50.5 L 70 45.5 - - M 4 110 V 14 H 52 V 110 Z - M 52 14 L 68 7 V 103 L 52 110 Z - M 4 14 L 20 7 H 68 L 52 14 Z - M 22 110 V 100 A 6 6 0 0 1 34 100 V 110 Z - + M 15 80 V 67 A 6 6 0 0 1 27 67 V 80 Z + M 22.5 124 V 106.5 H 37.5 V 124 Z M 0 0 H 6 L 2 9 M 4.5 9 V 0 L 0 6 H 6.5 M 1 3.2 A 2.7 2.7 0 1 1 5.4 5.3 C 4.2 6.1 3.6 6.6 3.6 7.8 M 3.6 10.6 V 10.8 M 12 20.5 C 12 20.5 3 14.6 3 8.7 A 4.6 4.6 0 0 1 12 6.6 A 4.6 4.6 0 0 1 21 8.7 C 21 14.6 12 20.5 12 20.5 Z - M 0 13 L 6 6 L 9.5 9.5 L 12.5 6.5 L 18 13 Z + M 5 0 Q 5.7 4.3 10 5 Q 5.7 5.7 5 10 Q 4.3 5.7 0 5 Q 4.3 4.3 5 0 Z + M 4 18 A 6 6 0 0 1 10 8.5 A 8.5 8.5 0 0 1 25 5 A 7 7 0 0 1 36 11 A 4.6 4.6 0 0 1 41 18 Z + M 3.5 30 L 24.5 9 V 17 L 11.5 30 Z + M 5 25 L 26.5 3.5 H 36 L 11 29 Z + M 32 4 A 28 28 0 0 1 59.6 36.9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M 45 -1 A 46 46 0 0 1 84.84 68 M 45 91 A 46 46 0 0 1 5.16 22 M 0 0 C 0 0.62 1 0.38 1 1 M 1 0 C 1 0.62 0 0.38 0 1 diff --git a/src/MeWeb.App/Resources/Colors.axaml b/src/MeWeb.App/Resources/Colors.axaml index e8c7040..1051c2d 100644 --- a/src/MeWeb.App/Resources/Colors.axaml +++ b/src/MeWeb.App/Resources/Colors.axaml @@ -25,6 +25,56 @@ #1FF0EAEF #00F0EAEF + #0D0911 + #2E1834 + #5CC2456F + #00C2456F + #70EC7A82 + #00EC7A82 + #48324F + #150E18 + #4DF2C14E + #B30A070D + #000A070D + #A60A070D + #8E7695 + #5F4B67 + #4D3C56 + #352840 + #D4C3D6 + #73D4C3D6 + #661B141C + #33202F + #FFE9AE + #231A29 + #8DB6EE + #5C5779 + #3F3A58 + #2C283E + #746D93 + #3E8670 + #1F4C44 + #4C3137 + #4F4459 + #211B26 + #A898AD + #41365A + #16111C + #26F0EAEF + #F7B27A + #26362848 + #6B8A4470 + #80B3A6B2 + #00B3A6B2 + #38F0EAEF + #0AF0EAEF + #B4F0CF + #996FCF97 + #006FCF97 + #FBE3A1 + #88F06B79 + #00F06B79 + @@ -42,4 +92,25 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MeWeb.App/Resources/Styles.axaml b/src/MeWeb.App/Resources/Styles.axaml index 35734ba..6bba3f6 100644 --- a/src/MeWeb.App/Resources/Styles.axaml +++ b/src/MeWeb.App/Resources/Styles.axaml @@ -124,6 +124,11 @@ + + + + + + @@ -293,6 +301,127 @@ + + + + + + + + + + + + + @@ -478,14 +607,6 @@ - - - - + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + - + + + + - - - - - + + + + + + + - - - - diff --git a/src/MeWeb.App/Views/IntroScene.axaml b/src/MeWeb.App/Views/IntroScene.axaml index 8f76fbf..ee8f1b8 100644 --- a/src/MeWeb.App/Views/IntroScene.axaml +++ b/src/MeWeb.App/Views/IntroScene.axaml @@ -5,102 +5,145 @@ FlowDirection="LeftToRight" Width="360" Height="212" ClipToBounds="True"> + + - + + + + + + + + + - - - - + + + + + + + + + + + + + - - - - - + + + - - + + + + + + - - - - - - - - + + + + + + + + - - - - - + + - - + + + - - - - - + + + + + - - - - + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + - - - - + + + + + + - - - + + - + - - + + + + diff --git a/src/MeWeb.App/Views/IntroScene.axaml.cs b/src/MeWeb.App/Views/IntroScene.axaml.cs index 19276ab..0b23bf9 100644 --- a/src/MeWeb.App/Views/IntroScene.axaml.cs +++ b/src/MeWeb.App/Views/IntroScene.axaml.cs @@ -14,54 +14,96 @@ public partial class IntroScene : Animated const double Radius = 290; const double Loop = 8; - static readonly (double X, double Y, double Pace)[] StarSpots = - [ - (22, 18, 1.1), (64, 46, 0.7), (98, 12, 1.4), (140, 34, 0.9), (205, 14, 1.2), (250, 42, 0.8), (292, 16, 1.3), - (334, 50, 1.0), (318, 94, 0.6), (30, 88, 1.2), (110, 72, 0.9), (268, 86, 1.1), (350, 20, 0.7), (176, 8, 1.0), - ]; + static readonly double[] Hamlet = [9, 23, 337, 351]; + static readonly int[] Search = [5, 0, 10, 3, 8, 1, 11, 6, 2, 9, 4, 7]; + static readonly int[] Television = [3, 10]; + static readonly double[][] Groves = [[112, 126, 244], [104, 117, 284], [292, 306, 186]]; + static readonly double[][] Growth = [[1, 0.72, 0.95], [1, 0.72, 0.95], [1, 0.72, 0.8]]; - static readonly double[] FlatLight = [0.9, 0.25, 0.7, 0.35, 0.85, 0.2, 0.6, 0.9, 0.3, 0.75, 0.4, 0.8]; - - readonly Ellipse[] stars; + readonly (Control Star, double Pace, double Shift, double Glow)[] stars; + readonly (Control Light, double Pace, double Shift)[] lights; readonly Rectangle[] flats; + readonly Ellipse[] puffs; + readonly Ellipse[] sparks; + readonly double[] lean = new double[3]; Point from; Point home; Point to; Point bend; Point turn; - double heartTop; + Point chimney; + Point heart; + Point badge; + Point doubt; + Point plate; + Point callout; public IntroScene() { InitializeComponent(); - stars = StarSpots.Select(spot => + stars = Enumerable.Range(1, 56) + .Select(i => new Point(Fraction(i * 0.7548776662) * 356 + 2, Fraction(i * 0.5698402910) * 160 + 3)) + .Where(spot => spot.Y < Ground(spot.X) - 16) + .Select((spot, i) => + { + var bright = i % 6 == 2; + var size = bright ? 10 : 0.9 + 1.5 * Fraction(i * 0.381966) * Fraction(i * 0.381966); + var star = bright ? (Control)new Avalonia.Controls.Shapes.Path() : new Ellipse { Width = size, Height = size }; + star.Classes.Add(bright ? "sparkle" : "star"); + star.RenderTransform = new ScaleTransform(); + Canvas.SetLeft(star, spot.X - size / 2); + Canvas.SetTop(star, spot.Y - size / 2); + Sky.Children.Add(star); + return (star, 0.6 + 1.6 * Fraction(i * 0.2763932), i * 2.39996, 0.3 + 0.7 * Fraction(i * 0.6180339887)); + }) + .ToArray(); + lights = Enumerable.Range(0, 7).SelectMany(town => { - var star = new Ellipse { Width = 2.4, Height = 2.4 }; - star.Classes.Add("star"); - Canvas.SetLeft(star, spot.X); - Canvas.SetTop(star, spot.Y); - Sky.Children.Add(star); - return star; + var x = 22 + town * 53 + 10 * Math.Sin(town * 2.1); + var y = Math.Min(Ground(x) + 24 + 26 * Fraction(town * 0.618034), 204); + var glow = new Ellipse { Width = 46, Height = 16 }; + glow.Classes.Add("townGlow"); + Canvas.SetLeft(glow, x - 23); + Canvas.SetTop(glow, y - 8); + Towns.Children.Add(glow); + return Enumerable.Range(1, 6).Select(i => + { + var size = 0.9 + 1.1 * Fraction((town * 6 + i) * 0.381966); + var light = new Ellipse { Width = size, Height = size }; + light.Classes.Add("town"); + Canvas.SetLeft(light, x + 30 * (Fraction((town * 6 + i) * 0.7548776662) - 0.5)); + Canvas.SetTop(light, y + 9 * (Fraction((town * 6 + i) * 0.5698402910) - 0.5)); + Towns.Children.Add(light); + return ((Control)light, 0.4 + 0.9 * Fraction(i * 0.2763932), town * 1.3 + i * 2.39996); + }); }).ToArray(); flats = Enumerable.Range(0, 12).Select(i => { - var flat = new Rectangle { Width = 8, Height = 9 }; + var flat = new Rectangle { Width = 8, Height = 11, RadiusX = 1, RadiusY = 1 }; flat.Classes.Add("flat"); - Canvas.SetLeft(flat, 10 + i % 3 * 14); - Canvas.SetTop(flat, 22 + i / 3 * 16); + if (i is 1 or 5 or 8) flat.Classes.Add("dark"); + if (Television.Contains(i)) flat.Classes.Add("cool"); + Canvas.SetLeft(flat, 12 + i % 3 * 14); + Canvas.SetTop(flat, 24 + i / 3 * 17); Flats.Children.Add(flat); return flat; }).ToArray(); - foreach (var mover in new Control[] { Photo, Packet, Heart, Yes, Unsure, Callout }) - { - mover.RenderTransform = new ScaleTransform(); - mover.RenderTransformOrigin = RelativePoint.Center; - } + puffs = Enumerable.Range(0, 5).Select(_ => Speck(Smoke, "smoke", 10)).ToArray(); + sparks = Enumerable.Range(0, 7).Select(_ => Speck(Sparks, "spark", 3.4)).ToArray(); + foreach (var mover in new Control[] { Photo, Letter, Heart, Yes, Unsure, Callout, RippleOne, RippleTwo, Meteor } + .Concat(Clouds.Children).Concat(Hearts.Children)) mover.RenderTransform = Mobile(); + Orbit.RenderTransform = new RotateTransform(); + PhoneBody.RenderTransform = new TranslateTransform(); + TabletBody.RenderTransform = new TranslateTransform(); + Received.RenderTransform = new ScaleTransform(); + Outbox.RenderTransform = new TranslateTransform(); + Scan.RenderTransform = new TranslateTransform(); + ScanGlow.RenderTransform = new TranslateTransform(); Lay(); Place(Poster); } - protected override double Poster => Step switch { 0 => 4.5, 1 => 5.0, _ => 5.2 }; + protected override double Poster => Step switch { 0 => 4.7, 1 => 5.0, _ => 5.3 }; public int Step { @@ -81,50 +123,87 @@ public partial class IntroScene : Animated { var sharing = Step == 0; var single = Step == 1; - Tablet.IsVisible = Photo.IsVisible = Heart.IsVisible = Round.IsVisible = sharing; - Packet.IsVisible = Yes.IsVisible = Plate.IsVisible = !sharing; - Callout.IsVisible = CalloutLine.IsVisible = PlateGlow.IsVisible = single; + Tablet.IsVisible = Photo.IsVisible = Heart.IsVisible = Hearts.IsVisible = Viewfinder.IsVisible = sharing; + Letter.IsVisible = Yes.IsVisible = Plate.IsVisible = Outbox.IsVisible = DoorLight.IsVisible = DoorSpill.IsVisible = !sharing; + Callout.IsVisible = Beam.IsVisible = PlateGlow.IsVisible = single; Block.IsVisible = Unsure.IsVisible = Step == 2; TrailTwo.IsVisible = !single; - var phone = Stand(Phone, sharing ? 56 : 44, 12, 40, 1.25); - var house = Stand(House, Step switch { 0 => 180, 1 => 204, _ => 128 }, 40, 72, Step switch { 0 => 1.5, 1 => 1.6, _ => 1.25 }); - from = Turn(phone, 0, -44); - Canvas.SetLeft(Halo, house.At.X - 120); - Canvas.SetTop(Halo, house.At.Y - 130); + for (var i = 0; i < Hamlet.Length; i++) Stand(Hamlets.Children[i], Hamlet[i], 5, 10, 1); + for (var i = 0; i < lean.Length; i++) lean[i] = Stand(Trees.Children[i], Groves[Step][i], i == 2 ? 13 : 10, 36, Growth[Step][i]).Degrees; + var phone = Stand(Phone, sharing ? 60 : 48, 14, 56, 1.2, 0.5); + var house = Stand(House, Step switch { 0 => 180, 1 => 194, _ => 128 }, 42, 80, Step switch { 0 => 1.45, 1 => 1.5, _ => 1.2 }); + from = Turn(phone, 0, -54); + chimney = Turn(house, 12, -80); + var glow = Turn(house, 12, -42); + Canvas.SetLeft(Halo, glow.X - Halo.Width / 2); + Canvas.SetTop(Halo, glow.Y - Halo.Height / 2); if (sharing) { - var tablet = Stand(Tablet, 304, 22, 32, 1.25); - home = Turn(house, -6, -30); - to = Turn(tablet, 0, -16); - bend = new Point((from.X + home.X) / 2, 18); - turn = new Point((home.X + to.X) / 2, 18); - Put(Heart, Turn(tablet, 10, -44)); - TrailOne.Data = Curve(from, bend, home); - TrailTwo.Data = Curve(home, turn, to); + var tablet = Stand(Tablet, 300, 27, 46, 1.15, 0.5); + home = Turn(house, 24, -25); + to = Turn(tablet, 0, -27); + bend = new Point((from.X + home.X) / 2, 4); + turn = new Point((home.X + to.X) / 2, 10); + heart = Turn(tablet, 16, -58); + TrailOne.Data = Arc(from, bend, home); + TrailTwo.Data = Arc(home, turn, to); return; } - home = Turn(house, -12, -8); - bend = new Point((from.X + home.X) / 2, 40); - Put(Yes, Turn(house, -4, single ? -73 : -82)); - TrailOne.Data = Curve(from, bend, home); + home = Turn(house, -21, -9); + bend = new Point((from.X + home.X) / 2, single ? 22 : 40); + badge = Turn(house, -43, -34); + TrailOne.Data = Arc(from, bend, home); if (single) { - var window = Turn(house, 24, -26); - var bubble = new Point(318, 46); - Put(Callout, bubble); - CalloutLine.Data = Geometry.Parse(FormattableString.Invariant($"M {window.X:0.##} {window.Y:0.##} L {bubble.X - 22:0.##} {bubble.Y + 18:0.##}")); + plate = Turn(house, -11, -46); + callout = new Point(310, 56); + Beam.Data = Funnel(Turn(house, 24, -25), callout); return; } - var block = Stand(Block, 240, 34, 110, 0.95); - to = Turn(block, -6, -5); - turn = new Point((from.X + to.X) / 2, -14); - Put(Unsure, Turn(block, 2, -122)); - TrailTwo.Data = Curve(from, turn, to); + var block = Stand(Block, 248, 38, 124, 0.95); + to = Turn(block, -8, -9); + turn = new Point((from.X + to.X) / 2 + 20, -18); + doubt = Turn(block, -48, -42); + TrailTwo.Data = Arc(from, turn, to); } protected override void Place(double seconds) { - for (var i = 0; i < stars.Length; i++) stars[i].Opacity = 0.2 + 0.45 * (0.5 + 0.5 * Math.Sin(seconds * StarSpots[i].Pace + i * 1.7)); + foreach (var (star, pace, shift, glow) in stars) + { + var shine = 0.5 + 0.5 * Math.Sin(seconds * pace + shift); + star.Opacity = glow * (0.3 + 0.7 * shine); + if (star is Ellipse) continue; + var scale = (ScaleTransform)star.RenderTransform; + scale.ScaleX = scale.ScaleY = 0.35 + 0.45 * shine; + } + foreach (var (light, pace, shift) in lights) light.Opacity = 0.55 + 0.45 * Math.Sin(seconds * pace + shift); + for (var i = 0; i < Clouds.Children.Count; i++) + { + var drift = Fraction(i * 0.41 + seconds * (0.004 + 0.0025 * i)) * 470 - 55; + Move(Clouds.Children[i], new Point(drift, 46 + i * 24), 1.5 - 0.3 * i, 0, 0.55 + 0.2 * i); + } + for (var k = 0; k < puffs.Length; k++) + { + var rise = Fraction(seconds / 3.6 + (double)k / puffs.Length); + var at = new Point(chimney.X + rise * 11 + Math.Sin(rise * 5 + k) * 1.8, chimney.Y - rise * 32); + Move(puffs[k], at, 0.35 + 1.15 * rise, 0, 0.8 * Math.Sin(Math.PI * rise)); + } + for (var i = 0; i < lean.Length; i++) + { + var sway = (RotateTransform)((TransformGroup)Trees.Children[i].RenderTransform).Children[1]; + sway.Angle = lean[i] + 1.6 * Math.Sin(seconds * 1.15 + i * 2.1); + } + foreach (var i in Television) flats[i].Opacity = 0.55 + 0.45 * Math.Abs(Math.Sin(seconds * 5.3 + i) * Math.Sin(seconds * 2.9)); + ((TranslateTransform)PhoneBody.RenderTransform).Y = -2.5 - 1.8 * Math.Sin(seconds * 1.7); + ((TranslateTransform)TabletBody.RenderTransform).Y = -2.5 - 1.8 * Math.Sin(seconds * 1.7 + 2); + ((RotateTransform)Orbit.RenderTransform).Angle = seconds * 110 % 360; + Led.Opacity = Math.Sin(seconds * 13) * Math.Sin(seconds * 4.7) > 0.05 ? 1 : 0.25; + Beacon.Opacity = Math.Pow(0.5 + 0.5 * Math.Cos(seconds * 3.4), 6); + var streak = Fraction((seconds + 5) / 17) * 17 / 0.8; + Move(Meteor, new Point(310 - 140 * streak, 10 + 48 * streak), 1, 161, streak < 1 ? Math.Sin(Math.PI * streak) : 0); + Comet.Opacity = Photo.Opacity = Letter.Opacity = 0; + foreach (var spark in sparks) spark.Opacity = 0; var t = seconds % Loop; if (Step == 0) Share(t); else if (Step == 1) Explain(t); @@ -133,68 +212,169 @@ public partial class IntroScene : Animated void Share(double t) { - Flash.Opacity = 0.9 * Envelope(t, 0.25, 0.08, 0.35, 0.45); - var glow = Envelope(t, 2.05, 0.25, 2.7, 1.8); - WindowGlow.Opacity = 0.35 + 0.65 * glow; - Halo.Opacity = 0.55 + 0.45 * glow; - Received.Opacity = Envelope(t, 4.05, 0.3, 7.3, 0.5); - if (t is >= 0.5 and < 2.0) + Flash.Opacity = 0.95 * Envelope(t, 0.3, 0.06, 0.36, 0.45); + Ripple(RippleOne, t < 2 ? from : to, t < 2 ? (t - 0.32) / 0.8 : (t - 4.1) / 0.9); + Ripple(RippleTwo, home, (t - 1.95) / 0.8); + var inside = Envelope(t, 1.95, 0.2, 2.6, 1.6); + WindowGlow.Opacity = 0.35 + 0.65 * inside; + Halo.Opacity = 0.45 + 0.55 * inside; + if (t is >= 0.55 and < 2.0) Fly(Photo, from, bend, home, (t - 0.55) / 1.45, 0.3, true); + else if (t is >= 2.6 and < 4.15) Fly(Photo, home, turn, to, (t - 2.6) / 1.55, 0.3, true); + var shown = Envelope(t, 4.1, 0.3, 7.3, 0.5); + Received.Opacity = shown; + var reveal = (ScaleTransform)Received.RenderTransform; + reveal.ScaleX = reveal.ScaleY = 1.3 - 0.3 * EaseOut(Clamp((t - 4.1) / 0.7)); + TabletGlow.Opacity = shown * (0.75 + 0.25 * Math.Sin(t * 2.4)); + var love = t - 4.2; + Move(Heart, new Point(heart.X, heart.Y - 3 * Math.Sin(love * 2)), Spring(love), 10 * Math.Sin(love * 4) * Math.Exp(-love * 1.6), + Envelope(t, 4.2, 0.05, 6.9, 0.5)); + for (var k = 0; k < Hearts.Children.Count; k++) { - var u = (t - 0.5) / 1.5; - Show(Photo, Along(from, bend, home, Smooth(u)), 0.6 + 0.55 * Math.Sin(Math.PI * u), 1); + var rise = (t - 4.5 - k * 0.45) / 1.9; + var little = Hearts.Children[k]; + if (rise is < 0 or > 1) + { + little.Opacity = 0; + continue; + } + Move(little, new Point(heart.X + (k - 1) * 10 + Math.Sin(rise * 7 + k) * 3, heart.Y - 12 - rise * 30), + 0.3 + 0.12 * Math.Sin(Math.PI * rise), 0, 0.9 * Math.Sin(Math.PI * rise)); } - else if (t is >= 2.0 and < 2.3) Show(Photo, home, 0.6 * (1 - (t - 2.0) / 0.3), 1); - else if (t is >= 2.55 and < 4.1) - { - var u = (t - 2.55) / 1.55; - Show(Photo, Along(home, turn, to, Smooth(u)), 0.45 + 0.6 * Math.Sin(Math.PI * u), 1); - } - else Photo.Opacity = 0; - var heart = (t - 4.15) / 1.7; - if (heart is < 0 or > 1) - { - Heart.Opacity = 0; - return; - } - Grow(Heart, Pop(heart * 3), heart < 0.6 ? 1 : 1 - (heart - 0.6) / 0.4); - Canvas.SetTop(Heart, heartTop - 16 * heart); } void Explain(double t) { - Led.Opacity = 0.35 + 0.65 * (0.5 + 0.5 * Math.Sin(t * 2 * Math.PI / 1.4)); - Grow(Callout, Pop((t - 0.2) / 0.5), Envelope(t, 0.2, 0.1, 7.3, 0.5)); - CalloutLine.Opacity = 0.8 * Envelope(t, 0.5, 0.3, 7.3, 0.5); - WindowGlow.Opacity = 0.35 + 0.65 * Envelope(t, 0.2, 0.4, 7.3, 0.5); - PlateGlow.Opacity = Envelope(t, 1.2, 0.3, 7.2, 0.6) * (0.75 + 0.25 * Math.Sin((t - 1.2) * 5)); - DoorLight.Opacity = 0.8 * Envelope(t, 3.7, 0.25, 7.2, 0.5); - Grow(Yes, Pop((t - 3.75) / 0.45), Envelope(t, 3.75, 0.1, 7.2, 0.5)); - if (t is >= 2.2 and < 3.7) Show(Packet, Along(from, bend, home, Smooth((t - 2.2) / 1.5)), 1, 1); - else Packet.Opacity = 0; + var open = Envelope(t, 0.2, 0.05, 7.2, 0.5); + Move(Callout, callout, Spring(t - 0.2), 0, open); + Beam.Opacity = Envelope(t, 0.5, 0.4, 7.2, 0.5); + WindowGlow.Opacity = 0.4 + 0.6 * open; + Halo.Opacity = 0.5 + 0.3 * open; + PlateGlow.Opacity = Envelope(t, 1.2, 0.3, 7.2, 0.6) * (0.75 + 0.25 * Math.Sin((t - 1.2) * 4)); + Ripple(RippleTwo, plate, (t - 1.25) / 0.9); + Send(t, 2.2); + if (t is >= 2.2 and < 3.7) Fly(Letter, from, bend, home, (t - 2.2) / 1.5, 0.5, true); + var admitted = Envelope(t, 3.6, 0.2, 7.2, 0.5); + DoorLight.Opacity = 0.9 * admitted; + DoorSpill.Opacity = admitted; + Ripple(RippleOne, home, (t - 3.6) / 0.8); + Badge(Yes, badge, t - 3.75, Envelope(t, 3.75, 0.05, 7.2, 0.5)); } void Knock(double t) { - DoorLight.Opacity = 0.8 * Envelope(t, 1.8, 0.25, 7.2, 0.5); - Grow(Yes, Pop((t - 1.85) / 0.45), Envelope(t, 1.85, 0.1, 7.2, 0.5)); - Grow(Unsure, Pop((t - 4.45) / 0.45), Envelope(t, 4.45, 0.1, 7.2, 0.5)); - if (t is >= 0.4 and < 1.8) Show(Packet, Along(from, bend, home, Smooth((t - 0.4) / 1.4)), 1, 1); - else if (t is >= 2.6 and < 4.2) Show(Packet, Along(from, turn, to, Smooth((t - 2.6) / 1.6)), 1, 1); - else if (t is >= 4.2 and < 4.9) + Halo.Opacity = 0.5; + WindowGlow.Opacity = 0.45; + Send(t, t < 2 ? 0.3 : 2.3); + if (t is >= 0.3 and < 1.6) Fly(Letter, from, bend, home, (t - 0.3) / 1.3, 0.5, true); + else if (t is >= 2.3 and < 3.9) Fly(Letter, from, turn, to, (t - 2.3) / 1.6, 0.5, false); + else if (t is >= 3.9 and < 6.9) { - var u = (t - 4.2) / 0.7; - Show(Packet, new Point(to.X - 7 * Math.Sin(Math.PI * u), to.Y - 3 * Math.Sin(Math.PI * u)), 1 - 0.5 * u, 1 - u); + var wait = t - 3.9; + var shake = Math.Sin(wait * 12) * Math.Exp(-wait * 1.1); + Move(Letter, new Point(to.X + 3 * shake, to.Y - 3 - 2 * Math.Sin(wait * 2.4)), 0.8, 8 * shake, 1 - Smooth((t - 6.3) / 0.5)); } - else Packet.Opacity = 0; - var searching = t is >= 4.45 and < 6.6 ? (int)((t - 4.45) / 0.16) % flats.Length : -1; - for (var i = 0; i < flats.Length; i++) flats[i].Opacity = i == searching ? 1 : FlatLight[i] * 0.8; + var admitted = Envelope(t, 1.55, 0.2, 7.2, 0.5); + DoorLight.Opacity = 0.9 * admitted; + DoorSpill.Opacity = admitted; + Ripple(RippleOne, home, (t - 1.55) / 0.8); + Badge(Yes, badge, t - 1.7, Envelope(t, 1.7, 0.05, 7.2, 0.5)); + Ripple(RippleTwo, to, (t - 3.9) / 0.8); + var waiting = Envelope(t, 3.9, 0.2, 6.6, 0.6); + GateLight.Opacity = 0.7 * waiting * (0.6 + 0.4 * Math.Sin(t * 6)); + BlockPlateGlow.Opacity = waiting; + var look = t is >= 4.2 and < 6.6 ? Search[(int)((t - 4.2) / 0.2) % Search.Length] : -1; + Scan.Opacity = ScanGlow.Opacity = look < 0 ? 0 : 1; + if (look >= 0) + { + var x = 12 + look % 3 * 14; + var y = 24 + look / 3 * 17; + foreach (var lit in new[] { (TranslateTransform)Scan.RenderTransform, (TranslateTransform)ScanGlow.RenderTransform }) + { + lit.X = x; + lit.Y = y; + } + } + Badge(Unsure, doubt, t - 4.4, Envelope(t, 4.4, 0.05, 7.2, 0.5)); } - (Point At, double Degrees, double Size) Stand(Control art, double x, double baseX, double baseY, double size) + void Send(double t, double leaving) { - var dx = x - CenterX; - var at = new Point(x, CenterY - Math.Sqrt(Radius * Radius - dx * dx) + 1); - var degrees = Math.Asin(dx / Radius) * 180 / Math.PI; + var gone = Smooth((t - leaving + 0.25) / 0.3); + var back = Smooth((t - leaving - 0.6) / 0.4); + ((TranslateTransform)Outbox.RenderTransform).Y = back > 0 ? 8 * (1 - back) : -14 * gone; + Outbox.Opacity = back > 0 ? back : 1 - gone; + } + + void Fly(Control mover, Point a, Point c, Point b, double u, double tilt, bool enters) + { + var e = Ease(u); + var at = Along(a, c, b, e); + var heading = (c - a) * (1 - e) + (b - c) * e; + var degrees = Math.Clamp(Math.Atan2(heading.Y, heading.X) * 180 / Math.PI, -70, 70) * tilt; + var lift = 0.9 + 0.3 * Math.Sin(Math.PI * u); + var land = enters ? 1 - 0.65 * Smooth((u - 0.82) / 0.18) : 1 - 0.2 * Smooth((u - 0.8) / 0.2); + Move(mover, at, (0.45 + 0.55 * Smooth(u / 0.15)) * land * lift, degrees, enters ? 1 - Smooth((u - 0.92) / 0.08) : 1); + var tail = Math.Max(0, e - 0.32); + var start = Along(a, c, b, tail); + Comet.Data = Arc(start, Blossom(a, c, b, tail, e), at); + var brush = (LinearGradientBrush)Comet.Stroke; + brush.StartPoint = new RelativePoint(start, RelativeUnit.Absolute); + brush.EndPoint = new RelativePoint(at, RelativeUnit.Absolute); + var trail = Smooth(u / 0.1) * (1 - Smooth((u - 0.8) / 0.2)); + Comet.Opacity = trail; + for (var k = 0; k < sparks.Length; k++) + { + var back = e - 0.045 * (k + 1); + if (back < 0) break; + var dust = Along(a, c, b, back); + var jitter = new Vector(Math.Sin(k * 2.3 + u * 17) * 2.6, Math.Cos(k * 1.7 + u * 13) * 2.6); + Move(sparks[k], dust + jitter, 1 - 0.11 * k, 0, trail * (1 - (double)k / sparks.Length)); + } + } + + static void Ripple(Control ring, Point at, double u) + { + if (u is < 0 or > 1) + { + ring.Opacity = 0; + return; + } + Move(ring, at, 0.2 + 1.1 * EaseOut(u), 0, 0.85 * (1 - u) * (1 - u)); + } + + static void Badge(Control badge, Point at, double since, double opacity) => + Move(badge, new Point(at.X, at.Y - 2 * Math.Sin(since * 2.2)), Spring(since), 0, opacity); + + static Ellipse Speck(Canvas layer, string kind, double size) + { + var speck = new Ellipse { Width = size, Height = size, RenderTransform = Mobile(), Opacity = 0 }; + speck.Classes.Add(kind); + Canvas.SetLeft(speck, -size / 2); + Canvas.SetTop(speck, -size / 2); + layer.Children.Add(speck); + return speck; + } + + static TransformGroup Mobile() => new() { Children = { new ScaleTransform(), new RotateTransform(), new TranslateTransform() } }; + + static void Move(Control mover, Point at, double size, double degrees, double opacity) + { + var group = (TransformGroup)mover.RenderTransform; + var scale = (ScaleTransform)group.Children[0]; + scale.ScaleX = scale.ScaleY = Math.Max(0, size); + ((RotateTransform)group.Children[1]).Angle = degrees; + var shift = (TranslateTransform)group.Children[2]; + shift.X = at.X; + shift.Y = at.Y; + mover.Opacity = opacity; + } + + static double Ground(double x) => CenterY - Math.Sqrt(Radius * Radius - (x - CenterX) * (x - CenterX)); + + static (Point At, double Degrees, double Size) Stand(Control art, double x, double baseX, double baseY, double size, double tilt = 1) + { + var at = new Point(x, Ground(x) + 1); + var degrees = tilt * Math.Asin((x - CenterX) / Radius) * 180 / Math.PI; Canvas.SetLeft(art, at.X - baseX); Canvas.SetTop(art, at.Y - baseY); art.RenderTransformOrigin = new RelativePoint(baseX, baseY, RelativeUnit.Absolute); @@ -210,13 +390,55 @@ public partial class IntroScene : Animated return new Point(stand.At.X + x * Math.Cos(radians) - y * Math.Sin(radians), stand.At.Y + x * Math.Sin(radians) + y * Math.Cos(radians)); } - static Geometry Curve(Point a, Point c, Point b) => - Geometry.Parse(FormattableString.Invariant($"M {a.X:0.##} {a.Y:0.##} Q {c.X:0.##} {c.Y:0.##} {b.X:0.##} {b.Y:0.##}")); + static Geometry Arc(Point a, Point c, Point b) + { + var geometry = new StreamGeometry(); + using var context = geometry.Open(); + context.BeginFigure(a, false); + context.QuadraticBezierTo(c, b); + context.EndFigure(false); + return geometry; + } + + static Geometry Funnel(Point from, Point to) + { + var along = (Vector)(to - from); + var across = new Vector(-along.Y, along.X) / along.Length; + var geometry = new StreamGeometry(); + using var context = geometry.Open(); + context.BeginFigure(from + across * 3, true); + context.LineTo(to + across * 30); + context.LineTo(to - across * 30); + context.LineTo(from - across * 3); + context.EndFigure(true); + return geometry; + } static Point Along(Point a, Point c, Point b, double u) => new((1 - u) * (1 - u) * a.X + 2 * (1 - u) * u * c.X + u * u * b.X, (1 - u) * (1 - u) * a.Y + 2 * (1 - u) * u * c.Y + u * u * b.Y); - static double Smooth(double u) => u * u * (3 - 2 * u); + static Point Blossom(Point a, Point c, Point b, double s, double u) => + new((1 - s) * (1 - u) * a.X + ((1 - s) * u + s * (1 - u)) * c.X + s * u * b.X, (1 - s) * (1 - u) * a.Y + ((1 - s) * u + s * (1 - u)) * c.Y + s * u * b.Y); + + static double Fraction(double x) => x - Math.Floor(x); + + static double Clamp(double u) => Math.Clamp(u, 0, 1); + + static double Smooth(double u) + { + u = Clamp(u); + return u * u * (3 - 2 * u); + } + + static double Ease(double u) + { + u = Clamp(u); + return u < 0.5 ? 4 * u * u * u : 1 - Math.Pow(2 - 2 * u, 3) / 2; + } + + static double EaseOut(double u) => 1 - Math.Pow(1 - Clamp(u), 3); + + static double Spring(double since) => since <= 0 ? 0 : 1 - Math.Exp(-7 * since) * Math.Cos(13 * since); static double Envelope(double t, double start, double rise, double end, double fall) { @@ -224,26 +446,4 @@ public partial class IntroScene : Animated if (t < start + rise) return (t - start) / rise; return t <= end ? 1 : 1 - (t - end) / fall; } - - static double Pop(double u) => u <= 0 ? 0 : u >= 1 ? 1 : u < 0.6 ? 1.18 * u / 0.6 : 1.18 - 0.18 * (u - 0.6) / 0.4; - - void Put(Control mover, Point at) - { - Canvas.SetLeft(mover, at.X - mover.Width / 2); - Canvas.SetTop(mover, at.Y - mover.Height / 2); - if (mover == Heart) heartTop = at.Y - mover.Height / 2; - } - - void Show(Control mover, Point at, double size, double opacity) - { - Put(mover, at); - Grow(mover, size, opacity); - } - - static void Grow(Control mover, double size, double opacity) - { - mover.Opacity = opacity; - var scale = (ScaleTransform)mover.RenderTransform; - scale.ScaleX = scale.ScaleY = Math.Max(0, size); - } } diff --git a/src/MeWeb.App/Views/MainView.axaml b/src/MeWeb.App/Views/MainView.axaml index 13f354b..453465b 100644 --- a/src/MeWeb.App/Views/MainView.axaml +++ b/src/MeWeb.App/Views/MainView.axaml @@ -114,7 +114,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -234,6 +234,10 @@ Classes.flowing="{Binding IsRunning}" Classes.lit="{Binding IsLit}" /> + + @@ -269,10 +273,18 @@ AutomationProperties.AutomationId="{Binding AutomationId}" AutomationProperties.Name="{Binding Title}"> + + + +