Compare commits
12
Commits
car-2.46.0
...
car-2.51.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
142f016174 | ||
|
|
3cad9499fb | ||
|
|
60a31574a3 | ||
|
|
3a9b6e17c8 | ||
|
|
16b11405c8 | ||
|
|
3bb0d5eb71 | ||
|
|
bf6038fd37 | ||
|
|
5cbd6699aa | ||
|
|
ff204caa52 | ||
|
|
69ccf0d5fd | ||
|
|
d80221db15 | ||
|
|
83dc8b231d |
@@ -77,7 +77,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`),
|
||||
|
||||
| Tab | Contents |
|
||||
|---|---|
|
||||
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed, and under the battery what plays: `NowPlaying` picks from the media sessions `NowPlayingWatcher` follows — listened to only, never connected to, nothing logged or sent; Tiles, Día de Muertos and Halloween, Settings → "Show what's playing", `PREF_NOW_PLAYING`, on by default), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`), Grid with map (the same `GridView` over the roads round the car, top-down: GPS while it is shown, tiles from the server's `/v1/map/roads` through `MapRoads`, cached in `files/map`; `RoadTile`, `MapGeometry`; the map is `MapLayerView`, a hardware layer drawn once and moved and turned by the GPU every display frame), Time circuits (`TimeCircuitsView`, the time machine's LED rows with `Segments14` letters and a flux capacitor), Quest (`QuestView`, a fantasy game interface), Visor (`VisorView`, a helmet display with a motion tracker of the power trail), Empire (`EmpireView`, a strategy game with an isometric world and ages by odometer, `EmpireAge`), Día de Muertos (`MuertosView`, an ofrenda: papel picado, candles, a calavera in a ring of marigolds), Halloween (`HalloweenView`, a jack-o'-lantern, a potion flask, a tombstone on a graveyard hill), both counting with `Tally`, the pumpkin's light flaring on the kick drum while on screen, when "Pumpkin light follows the music" is on (off by default, `PREF_MUSIC_LIGHT`; `MusicTap`, `MusicPulse`: Android's audio analyser, 40–120 Hz, jumps not levels; Bluetooth and CarPlay, not the radio); the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie, Time circuits, Quest, Empire, Día de Muertos, Halloween; Retro, Terminal, Grid and Visor with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
|
||||
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed, and under the battery what plays: `NowPlaying` picks from the media sessions `NowPlayingWatcher` follows — listened to only, never connected to, nothing logged or sent; Tiles, Terminal, Sci-fi, Quest, Empire, Día de Muertos, Halloween, Christmas and Caterpillar, Settings → "Show what's playing", `PREF_NOW_PLAYING`, on by default), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`), Grid with map (the same `GridView` over the roads round the car, top-down: GPS while it is shown, tiles from the server's `/v1/map/roads` through `MapRoads`, cached in `files/map`; `RoadTile`, `MapGeometry`; the map is `MapLayerView`, a hardware layer drawn once and moved and turned by the GPU every display frame), Time circuits (`TimeCircuitsView`, the time machine's LED rows with `Segments14` letters and a flux capacitor), Quest (`QuestView`, a fantasy game interface), Visor (`VisorView`, a helmet display with a motion tracker of the power trail), Empire (`EmpireView`, a strategy game with an isometric world and ages by odometer, `EmpireAge`), Día de Muertos (`MuertosView`, an ofrenda: papel picado, candles, a calavera in a ring of marigolds), Halloween (`HalloweenView`, a jack-o'-lantern, a potion flask, a tombstone on a graveyard hill), Christmas (`ChristmasView`, a tree with a bauble, fairy lights, a mug of cocoa, a wreath clock and candy canes in the snow), Caterpillar (`CaterpillarView`, cut-paper collage after a picture book: the caterpillar is speed and power, the sun the battery, fruit the range), all counting with `Tally`, the pumpkin's light, Christmas's tree lights and the Caterpillar's sun flaring on the kick drum while on screen, when "Light follows the music" is on (off by default, `PREF_MUSIC_LIGHT`; `MusicTap`, `MusicPulse`: Android's audio analyser, 40–120 Hz, jumps not levels; Bluetooth and CarPlay, not the radio); the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie, Time circuits, Quest, Empire, Día de Muertos, Halloween, Christmas, Caterpillar; Retro, Terminal, Grid and Visor with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
|
||||
| Consumption / Charging | period totals, daily log, charging sessions and curve |
|
||||
| Live data | on/off, ABRP API key and user token, upload frequency |
|
||||
| Server | URL, pairing by QR, ingest token, installation ID (read-only), config import |
|
||||
|
||||
@@ -140,6 +140,18 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
|
||||
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
|
||||
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
|
||||
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
|
||||
- 2026-11-07 — App 2.51.0: **the Caterpillar after the book's newer pictures** (owner sent a 3D picture of it). Plump round segments shaded like balls of wound paper (radial light from the upper left) with brush strokes in light green and teal, bigger towards the head, a fringe of yellow and orange bristles along the back, stubby brown feet; a big round red head with teal eyes rimmed in yellow and two lilac ears, the speed under the eyes. Crawl, readings and colours (green lit, blue regenerating, dull dark) unchanged. Original drawing, not the artwork. Tested: compiles, emulator screenshots with sample values (`screenshots/themes/caterpillar*.png`). Not tried on the car.
|
||||
- 2026-11-07 — App 2.50.0 published (`car-2.50.0`).
|
||||
- 2026-11-07 — App 2.50.0: **the Caterpillar crawls** (owner: "make it so the caterpillar appears to be going when we drive? Using normal caterpillar movement?"). While the car moves it crawls as a caterpillar does: a hump runs from the tail to the head, the body bunches and stretches a little, feet that are lifted swing forward and the others stay planted on a branch whose knots and leaves slide backwards with the speed; the crawl is quicker the faster the car goes (at most 1.2 crawls a second). It does not change its place on screen, so the readings stay put. Parked, it eases back into its high arch (it keeps drawing for that moment only). Two screenshots redone. Tested: unit tests, lint, emulator with sample values (frames 0.25 s apart show the hump travelling and the branch sliding). Not tried on the car.
|
||||
- 2026-11-07 — App 2.49.1 published (`car-2.49.1`).
|
||||
- 2026-11-07 — App 2.49.1: **the Caterpillar looks more like the one in the book** (owner: "Make it more like the original caterpillar", with the German edition's cover as the reference). It arches over the middle like the cover's: overlapping chunks of mottled green and teal paper, a yellow fringe along the back, olive feet, and a red-orange head with a yellow face, green eyes and purple antennae; the speed is on the face. Power still lights the segments (blue when regenerating) and the back humps while driving. Still my own drawing of it, not the book's artwork. Two screenshots redone. Tested: unit tests, lint, emulator with sample values. Not tried on the car.
|
||||
- 2026-11-07 — App 2.49.0 published (`car-2.49.0`).
|
||||
- 2026-11-07 — App 2.49.0: **the Caterpillar style** (owner: "a theme based on The Very Hungry Caterpillar"; design agreed first, defaults taken: dark night page, named "Caterpillar" not the book's title, fruit as range). `CaterpillarView`, an original cut-paper collage, not the book's illustrations: the caterpillar's red head is the speed and its ten segments the power (blue while regenerating, wiggling while driving); the sun is the battery (a grey moon below 20 %); a leaf holds trip, usage, climate and odometer; a clock in a paper circle; fruit in the story's order (1 apple, 2 pears, 3 plums, 4 strawberries, 5 oranges) is the range, one per 50 km, used ones with a bite out; P R N D are an egg, the caterpillar, a cocoon and a butterfly; hills are the last two minutes of power. The music light (now also for this style) bounces the sun's rays and the segments on the kick drum. What plays: a butterfly and, on the next pass, a ladybird tow a strip of tissue paper (colour fixed by the song); both in `SongCarriers`. Own palette (green). Two screenshots in THEMES.md. Tested: unit tests, lint, emulator with sample values and with the throwaway fake song. Not tried on the car.
|
||||
- 2026-11-07 — App 2.48.0 published (`car-2.48.0`).
|
||||
- 2026-11-07 — App 2.48.0: **the Christmas style** (owner: "let's also do a christmas based theme", with music and something that pulsates; design agreed first). `ChristmasView`: a tree with a big red bauble (speed), fairy lights arching over it (power: gold left to right while drawing, ice blue and right to left while regenerating), a glass mug of cocoa filling with the charge (cold blue and "BATTERY LOW" below 20 %), gift tags (trip, usage, climate, odometer), a wreath clock, a signboard (range) with a candy cane per 50 km, P R N D as baubles, and a snow drift of the last two minutes of power; snow falls and the lights twinkle while driving. Own palette (red); always in the drop-down; English only. The music light is now **"Light follows the music"** for Halloween and Christmas (same `PREF_MUSIC_LIGHT`, off by default): the tree's lights and the arch flare on the kick drum. What plays: Santa's sleigh with a reindeer and, on the next pass, an angel with a trumpet tow a strip of wrapping paper (colour fixed by the song); both in `SongCarriers`. Two screenshots in THEMES.md. Tested: unit tests, lint, emulator with sample values (panel and full screen, and with no data), with the throwaway fake song (both carriers), and the music-tap thread runs only with the switch on. Not tried on the emulator: the pulse itself with real sound (the code path is Halloween's `MusicTap`). Not tried on the car.
|
||||
- 2026-11-02 — App 2.47.0 published (`car-2.47.0`).
|
||||
- 2026-11-02 — App 2.47.0: **now playing in four more styles** (owner: "let's implement Now playing in more styles"; Spanish labels declined). Quest: a red dragon tows a waving scroll across the top of the sky, behind the frames. Empire: an eagle flies a scroll across the top of the world, behind the speed. Terminal: a PLAYING row above the prompt. Sci-fi: the song in black on the top bar. The creatures are in the new `SongCarriers` (dragon, eagle) beside `SongBanner`. Same switch as before (Settings → "Show what's playing"). Not yet in the rest: Gauges, Cockpit, Retro, Futuristic, MG, Minimal, Nixie, Chronograph, Grid, Grid with map, Time circuits, Visor, Cartoon. Tested: unit tests, lint, emulator with the throwaway fake session. Not tried on the car.
|
||||
- 2026-11-02 — App 2.46.0 published (`car-2.46.0`).
|
||||
- 2026-11-02 — App 2.46.0: **Día de Muertos shows what plays** (owner: "Can we also put that into the dia de muertos theme?", then "switch between monarch and alebrije", "one color per song"). A strip of papel picado (zigzag-cut edges) with "♪ title — artist" waves across the sky between the flags and the ring, behind the ring and the clock; a monarch butterfly and an alebrije (a painted horned creature with feathered wings and a curled tail) tow it on alternate passes. The paper's colour is one of the flags', fixed by the song (hash of title and artist, never logged). The banner moved out of `HalloweenView` into the shared `SongBanner`; Halloween looks as before. Same switch as Tiles and Halloween (Settings → "Show what's playing"). Tested: unit tests, lint, emulator with the throwaway fake session. Not tried on the car.
|
||||
- 2026-11-02 — App 2.45.2 published (`car-2.45.2`).
|
||||
- 2026-11-02 — App 2.45.2: Halloween's song banner **waves** (owner: "Can it also be a bit wavy?"): waves run from the bat to the tail, still where the bat holds it and up to 7 units at the tail, and the text follows them (`drawTextOnPath`). Tested: emulator with the throwaway fake session, unit tests, lint. Not tried on the car.
|
||||
|
||||
@@ -11,7 +11,7 @@ emulator has no car; on the car the values are live.
|
||||
|
||||
**Colours.** Most styles are drawn in the accent colour you pick from the MG4 paints (Settings →
|
||||
Accent colour; shown here in Dynamic Red). Retro, Terminal, Grid (with or without the map) and Visor can also show their original
|
||||
colours, and Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest, Empire, Día de Muertos and Halloween always do; while one of those is shown, the whole app
|
||||
colours, and Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest, Empire, Día de Muertos, Halloween, Christmas and Caterpillar always do; while one of those is shown, the whole app
|
||||
(navigation, buttons, switches) takes on its colours too.
|
||||
|
||||
| Style | Look | Colours |
|
||||
@@ -36,6 +36,8 @@ colours, and Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest, Empire, Día de
|
||||
| [Empire](#empire) | a strategy game's interface | own |
|
||||
| [Día de Muertos](#día-de-muertos) | an ofrenda with papel picado and a calavera | own |
|
||||
| [Halloween](#halloween) | a jack-o'-lantern on a graveyard hill | own |
|
||||
| [Christmas](#christmas) | a Christmas tree in a snowy village | own |
|
||||
| [Caterpillar](#caterpillar) | a hungry caterpillar in cut-paper collage | own |
|
||||
|
||||
## Tiles
|
||||
|
||||
@@ -94,6 +96,7 @@ A starship console, after the look of the computer screens in 90s science-fictio
|
||||
"elbow" frames in orange and lilac, pill-shaped labels, condensed capitals and blocky bar graphs
|
||||
on black. Below 20 % charge the frames turn red: red alert. The app takes on the lilac of the
|
||||
lower frame.
|
||||
What plays (Bluetooth, CarPlay or the radio) is written in black on the top bar, cut to its length.
|
||||
|
||||

|
||||

|
||||
@@ -131,6 +134,7 @@ driving.
|
||||
As if a shell printed the car's state: `watch -n 0.2 vehicle-status` on a green-phosphor screen
|
||||
with faint scan lines, each value on a row with an ASCII bar, the gear as `[D]`, and a prompt with
|
||||
a blinking cursor. A low battery turns its row amber. In green (shown) or in the accent colour.
|
||||
What plays (Bluetooth, CarPlay or the radio) shows as a PLAYING row above the prompt, cut to the width.
|
||||
|
||||

|
||||

|
||||
@@ -206,6 +210,7 @@ energy bar; a minimap with the clock; the speed in large gold numbers over a cas
|
||||
are the trip, the usage and the climate; a chat window with system lines; P R N D as action
|
||||
buttons, and the odometer as the experience bar, a level per 1000 km. Below 20 % a raid warning
|
||||
flashes "Low battery!". The app turns gold with it.
|
||||
What plays (Bluetooth, CarPlay or the radio) crosses the top of the sky on a waving scroll towed by a red dragon, behind the frames, again and again while it plays.
|
||||
|
||||

|
||||

|
||||
@@ -234,6 +239,7 @@ while the ground and trees move past as fast as you drive and the speed stands l
|
||||
land. The carved panel at the bottom has P R N D as command buttons, the unit's portrait and
|
||||
stats, and a diamond minimap with the odometer and the way to the next age. Below 20 % it warns
|
||||
"Your battery is running low!". The app turns the player's blue with it.
|
||||
What plays (Bluetooth, CarPlay or the radio) crosses the top of the world on a waving scroll flown by an eagle, behind the speed, again and again while it plays.
|
||||
|
||||

|
||||

|
||||
@@ -277,3 +283,49 @@ pumpkin's orange with it.
|
||||
|
||||

|
||||

|
||||
|
||||
## Christmas
|
||||
|
||||
A snowy village at night, warm and festive. In the middle a Christmas tree with a big red bauble
|
||||
in front of it: the speed. Fairy lights arch over the tree: the power, left to right in warm gold
|
||||
while drawing, ice blue and right to left while regenerating. On the left a glass mug of hot cocoa
|
||||
fills with the charge, with marshmallows floating on it and steam rising; it turns cold blue with
|
||||
"BATTERY LOW" blinking below 20 %. Above it, gift tags hang from a garland with trip, usage,
|
||||
climate and odometer. On the right a wreath holds the clock, and a snowy signboard reads the range
|
||||
with a candy cane for every 50 km in the snow below; P R N D are four baubles, the one engaged lit.
|
||||
The snow drift along the bottom is the last two minutes of power, rising with it. While you drive the
|
||||
snow falls and the tree's lights twinkle. With "Light follows the music" on (Settings, shown with
|
||||
this style and Halloween; off by default) and Bluetooth or CarPlay music playing, the lights on the
|
||||
tree and the fairy lights flare on each kick drum; the radio cannot be heard, so they twinkle as
|
||||
usual then. What plays (Bluetooth, CarPlay or the radio) crosses the sky above the tree on a waving
|
||||
strip of wrapping paper towed by Santa's sleigh and, on the next pass, a small angel with a
|
||||
trumpet; the paper's colour is fixed by the song. The app turns red with it.
|
||||
|
||||

|
||||

|
||||
|
||||
## Caterpillar
|
||||
|
||||
After a picture book about a very hungry caterpillar, drawn anew in cut-paper shapes and bright
|
||||
tissue colours on the book's night page. The caterpillar arches over the middle, after the one on
|
||||
the newer pictures of the book: plump, round segments like balls of wound paper with brush strokes
|
||||
in light green and teal, a fringe of yellow and orange bristles along the back and stubby brown
|
||||
feet, and a big round red head with teal eyes rimmed in yellow and two lilac ears. The face
|
||||
carries the speed, and its ten segments are the power, lit from the head backwards while drawing and
|
||||
blue while regenerating. While you drive it crawls the way a caterpillar does: a hump runs from its
|
||||
tail to its head, the feet it lifts swing forward and the others stay planted, and the branch it
|
||||
walks on slides by, faster the faster the car goes. Parked, it settles into its high arch. On the left the sun is the battery,
|
||||
filling with the charge; below 20 % it turns into a grey moon and "BATTERY LOW" blinks. A big leaf
|
||||
above it holds trip, usage, climate and odometer on paper labels. On the right a clock in a paper
|
||||
circle, and the story's fruit, one apple, two pears, three plums, four strawberries and five
|
||||
oranges: a fruit per 50 km of range, those used up with a bite taken out. P R N D are the stages of
|
||||
its life, an egg, the caterpillar, a cocoon and a butterfly, the one engaged lit. The hills along
|
||||
the bottom are the last two minutes of power, rising with it. With "Light follows the music" on
|
||||
(Settings, shown with this style, Halloween and Christmas; off by default) and Bluetooth or CarPlay
|
||||
music playing, the sun's rays and the caterpillar's segments bounce on each kick drum; the radio
|
||||
cannot be heard. What plays (Bluetooth, CarPlay or the radio) crosses the sky on a waving strip of
|
||||
tissue paper towed by a butterfly and, on the next pass, a ladybird; the paper's colour is fixed by
|
||||
the song. The app turns green with it.
|
||||
|
||||

|
||||

|
||||
|
||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
||||
applicationId "com.stinnesbeck.nilsmg4"
|
||||
minSdk 27
|
||||
targetSdk 36
|
||||
versionCode 163
|
||||
versionName "2.46.0"
|
||||
versionCode 169
|
||||
versionName "2.51.0"
|
||||
|
||||
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
||||
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
||||
|
||||
@@ -0,0 +1,664 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RadialGradient;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The Caterpillar Overview style, after the picture book about a very hungry caterpillar (drawn
|
||||
* anew, not copied from it): cut-paper shapes in bright tissue colours on the book's night page.
|
||||
*
|
||||
* In the middle the caterpillar, arched like the one on the book's cover (overlapping chunks of
|
||||
* mottled green tissue paper, a yellow fringe along the back, a red-orange head with a yellow face):
|
||||
* the face carries the speed, and its ten segments are the power, lit from the head backwards while
|
||||
* drawing, blue while regenerating; while the car moves it crawls the way a caterpillar does: a hump runs from tail to head, its feet
|
||||
* lifting and planting, on a branch that slides by. On the left the sun is the battery, filling with the charge, and turning into a
|
||||
* grey moon with "BATTERY LOW" blinking below 20 %; above it a big leaf holds trip, usage, climate
|
||||
* and odometer on paper labels. On the right the clock in a paper circle, and the
|
||||
* story's fruit: one apple, two pears, three plums, four strawberries and five oranges, a fruit
|
||||
* per 50 km of range, those used up with a bite taken out. P R N D are the four stages of its life,
|
||||
* an egg, the caterpillar, a cocoon and a butterfly, the one engaged lit. The hills along the bottom
|
||||
* are the last two minutes of power, rising with it. Unread values show "—" and light nothing.
|
||||
* With music playing, the sun's rays and the caterpillar's segments bounce on the beat
|
||||
* ({@link MusicTap}). What plays crosses the sky on a banner of tissue paper towed by a butterfly
|
||||
* and, on the next pass, a ladybird ({@link NowPlaying}).
|
||||
*
|
||||
* The night, the stars and the far hills are drawn once into a bitmap per size. The caterpillar
|
||||
* wiggles (about 30 frames a second) only while the car moves and the view is on screen; the
|
||||
* pulse and the song keep it drawing while there are any.
|
||||
*/
|
||||
final class CaterpillarView extends ClusterCanvasView {
|
||||
|
||||
private static final int SEGMENTS = 10, RAYS = 14, FRUIT = 15;
|
||||
private static final int NIGHT_TOP = 0xFF050B22, NIGHT_LOW = 0xFF12305A, NIGHT_END = 0xFF1D4A5E;
|
||||
private static final int GREEN = 0xFF7ED957, GREEN_DARK = 0xFF2E8B3A, LEAF = 0xFF3FA34D, HEAD = 0xFFE53935;
|
||||
private static final int SUN = 0xFFFFA726, SUN_LIGHT = 0xFFFFD54F, MOON = 0xFFB8BCC8, ICE = 0xFF5CC8FF;
|
||||
private static final int PAPER = 0xFFFBF1DA, INK = 0xFF1B1530, BONE = 0xFFF6F0E4, YELLOW = 0xFFFFE066;
|
||||
private static final int[] TISSUE = { 0xFFE53935, 0xFF2B7BE0, 0xFF2EA84F, 0xFFFFC928, 0xFF8E4DD6, 0xFFFF8A1F };
|
||||
|
||||
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint image = new Paint(Paint.FILTER_BITMAP_FLAG);
|
||||
private final Path path = new Path();
|
||||
private final Path bite = new Path();
|
||||
private final SongBanner banner = new SongBanner();
|
||||
private final RectF r = new RectF();
|
||||
private final Typeface serif = Typeface.create("serif", Typeface.BOLD);
|
||||
private final Typeface face = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
||||
private final Typeface thin = Typeface.create("sans-serif-condensed", Typeface.NORMAL);
|
||||
|
||||
private Bitmap sky;
|
||||
private int skyW, skyH;
|
||||
private float time;
|
||||
/** Where the crawl is (whole numbers are one full crawl), how far the ground has gone by, and how much it crawls (0 to 1). */
|
||||
private float phase, travel, crawl;
|
||||
private long lastFrame;
|
||||
private MusicTap music;
|
||||
|
||||
CaterpillarView(Context context) {
|
||||
super(context);
|
||||
line.setStyle(Paint.Style.STROKE);
|
||||
line.setStrokeCap(Paint.Cap.ROUND);
|
||||
}
|
||||
|
||||
/** Where the sun and the caterpillar hear the music; without it, they do not follow any. */
|
||||
void setMusic(MusicTap tap) {
|
||||
music = tap;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
sky = null;
|
||||
skyW = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas c) {
|
||||
super.onDraw(c);
|
||||
int w = getWidth(), h = getHeight();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
float u = Math.min(h / 640f, w / 1100f);
|
||||
boolean moving = v.speedKmh != null && v.speedKmh > 0.5f;
|
||||
long now = SystemClock.uptimeMillis();
|
||||
float dt = lastFrame == 0 ? 0f : Math.min(0.2f, (now - lastFrame) / 1000f);
|
||||
lastFrame = moving || crawl > 0.01f ? now : 0;
|
||||
if (moving) {
|
||||
float kmh = Math.min(v.speedKmh, 130f);
|
||||
time += dt;
|
||||
// One crawl takes a second or two; faster the faster the car goes, and the ground slides by.
|
||||
phase = (phase + dt * Math.min(1.2f, 0.4f + kmh / 90f)) % 1000f;
|
||||
travel = (travel + dt * kmh * 1.1f * u) % 100000f;
|
||||
}
|
||||
// It settles into its resting arch when the car stops, and sets off again when it moves.
|
||||
crawl += ((moving ? 1f : 0f) - crawl) * Math.min(1f, dt * 4f);
|
||||
if (sky == null || skyW != w || skyH != h) buildSky(w, h, u);
|
||||
c.drawBitmap(sky, 0, 0, image);
|
||||
boolean song = drawSong(c, w, u);
|
||||
drawHills(c, w, h, u);
|
||||
float pulse = music == null ? 0f : music.pulse();
|
||||
drawSun(c, u, pulse);
|
||||
drawLeaf(c, u);
|
||||
drawClock(c, w, u);
|
||||
drawFruit(c, w, u);
|
||||
drawGears(c, w, u);
|
||||
drawBranch(c, w, u);
|
||||
drawCaterpillar(c, w, u, pulse);
|
||||
keepAnimating(moving || crawl > 0.01f || pulse > 0f || song, 33);
|
||||
}
|
||||
|
||||
private void label(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a) {
|
||||
text.setTypeface(tf);
|
||||
text.setTextSize(size);
|
||||
text.setTextAlign(a);
|
||||
text.setColor(colour);
|
||||
c.drawText(s, x, y, text);
|
||||
}
|
||||
|
||||
/** Text with a dark outline, to read over whatever is behind it. */
|
||||
private void outlined(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a, float u) {
|
||||
text.setStyle(Paint.Style.STROKE);
|
||||
text.setStrokeWidth(Math.max(3f * u, size * 0.12f));
|
||||
text.setStrokeJoin(Paint.Join.ROUND);
|
||||
label(c, s, x, y, size, 0xDD0A0608, tf, a);
|
||||
text.setStyle(Paint.Style.FILL);
|
||||
label(c, s, x, y, size, colour, tf, a);
|
||||
}
|
||||
|
||||
private boolean regen() {
|
||||
return v.powerKw != null && v.powerKw < -0.05f;
|
||||
}
|
||||
|
||||
// ---------- The night page ----------
|
||||
|
||||
private void buildSky(int w, int h, float u) {
|
||||
sky = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
||||
skyW = w;
|
||||
skyH = h;
|
||||
Canvas c = new Canvas(sky);
|
||||
Paint p = new Paint();
|
||||
p.setShader(new LinearGradient(0, 0, 0, h, new int[] { NIGHT_TOP, NIGHT_LOW, NIGHT_END },
|
||||
new float[] { 0f, 0.7f, 1f }, Shader.TileMode.CLAMP));
|
||||
c.drawRect(0, 0, w, h, p);
|
||||
Paint star = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
star.setColor(0xB0FFF6C8);
|
||||
for (int i = 0; i < 80; i++) {
|
||||
float x = ((i * 191 + 17) % 1009) / 1009f * w, y = ((i * 83 + 5) % 421) * u;
|
||||
c.drawCircle(x, y, (i % 4 == 0 ? 2.2f : 1.2f) * u, star);
|
||||
}
|
||||
// A crescent moon, as on the book's first night.
|
||||
Path moon = new Path(), cut = new Path();
|
||||
float mx = w * 0.3f, my = 150f * u;
|
||||
moon.addCircle(mx, my, 30f * u, Path.Direction.CW);
|
||||
cut.addCircle(mx + 13f * u, my - 6f * u, 27f * u, Path.Direction.CW);
|
||||
moon.op(cut, Path.Op.DIFFERENCE);
|
||||
Paint paper = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
paper.setColor(0xFFFFF3B0);
|
||||
c.drawPath(moon, paper);
|
||||
hills(c, w, h, h - 150f * u, 0xFF124A3A, 0.8f, u);
|
||||
hills(c, w, h, h - 100f * u, 0xFF1A5E45, 1.5f, u);
|
||||
}
|
||||
|
||||
private void hills(Canvas c, int w, int h, float base, int colour, float wave, float u) {
|
||||
Path hill = new Path();
|
||||
hill.moveTo(0, h);
|
||||
for (int i = 0; i <= 24; i++) {
|
||||
float x = w * i / 24f;
|
||||
hill.lineTo(x, base - (float) (Math.sin(i * wave) * 0.5 + Math.sin(i * wave * 0.37 + 1) * 0.5) * 22f * u);
|
||||
}
|
||||
hill.lineTo(w, h);
|
||||
hill.close();
|
||||
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
p.setColor(colour);
|
||||
c.drawPath(hill, p);
|
||||
}
|
||||
|
||||
// ---------- The hills in front: the last two minutes of power ----------
|
||||
|
||||
private void drawHills(Canvas c, int w, int h, float u) {
|
||||
float base = h - 44f * u, amp = 36f * u;
|
||||
float[] trail = v.powerTrail;
|
||||
path.reset();
|
||||
path.moveTo(0, h);
|
||||
int n = trail == null ? 0 : trail.length;
|
||||
if (n == 0) {
|
||||
path.lineTo(0, base);
|
||||
path.lineTo(w, base);
|
||||
} else {
|
||||
for (int i = 0; i < n; i++) {
|
||||
float p = trail[i];
|
||||
float height = Float.isNaN(p) ? 0f : Math.min(1f, Math.abs(p) / (p < 0 ? 40f : 60f));
|
||||
path.lineTo(i / (n - 1f) * w, base - height * amp);
|
||||
}
|
||||
}
|
||||
path.lineTo(w, h);
|
||||
path.close();
|
||||
fill.setColor(0xFF2E9A4E);
|
||||
c.drawPath(path, fill);
|
||||
if (n > 1) {
|
||||
line.setStrokeWidth(3f * u);
|
||||
for (int i = 1; i < n; i++) {
|
||||
float a = trail[i - 1], b = trail[i];
|
||||
if (Float.isNaN(a) || Float.isNaN(b)) continue;
|
||||
float ha = Math.min(1f, Math.abs(a) / (a < 0 ? 40f : 60f));
|
||||
float hb = Math.min(1f, Math.abs(b) / (b < 0 ? 40f : 60f));
|
||||
line.setColor(b < 0 ? ICE : YELLOW);
|
||||
c.drawLine((i - 1f) / (n - 1f) * w, base - ha * amp, i / (n - 1f) * w, base - hb * amp, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The sun (or the moon): the battery ----------
|
||||
|
||||
private void drawSun(Canvas c, float u, float pulse) {
|
||||
float cx = 178f * u, cy = 452f * u, rad = 100f * u;
|
||||
boolean low = v.socPercent != null && v.socPercent < 20f;
|
||||
boolean blinkOn = (SystemClock.uptimeMillis() / 500) % 2 == 0;
|
||||
float share = v.socPercent == null ? 0f : Math.max(0f, Math.min(1f, v.socPercent / 100f));
|
||||
// The rays, longer and stronger with the music; none for the moon.
|
||||
if (!low) {
|
||||
for (int i = 0; i < RAYS; i++) {
|
||||
double a = Math.PI * 2 * i / RAYS;
|
||||
float cos = (float) Math.cos(a), sin = (float) Math.sin(a);
|
||||
float tip = rad + (22f + pulse * 16f + (i % 2) * 8f) * u;
|
||||
float nx = -sin * 8f * u, ny = cos * 8f * u;
|
||||
path.reset();
|
||||
path.moveTo(cx + cos * (rad - 2f * u) + nx, cy + sin * (rad - 2f * u) + ny);
|
||||
path.lineTo(cx + cos * tip, cy + sin * tip);
|
||||
path.lineTo(cx + cos * (rad - 2f * u) - nx, cy + sin * (rad - 2f * u) - ny);
|
||||
path.close();
|
||||
fill.setColor(AccentColor.withAlpha(SUN_LIGHT, 0x60 + (int) (share * 0x9F)));
|
||||
c.drawPath(path, fill);
|
||||
}
|
||||
}
|
||||
// The disc: dark where the charge is gone, the sun's colour as high as it is full.
|
||||
fill.setColor(0xFF1E2748);
|
||||
c.drawCircle(cx, cy, rad, fill);
|
||||
if (v.socPercent != null) {
|
||||
float level = cy + rad - share * 2f * rad;
|
||||
c.save();
|
||||
c.clipRect(cx - rad, level, cx + rad, cy + rad + 1f);
|
||||
fill.setColor(low ? MOON : SUN);
|
||||
c.drawCircle(cx, cy, rad, fill);
|
||||
c.restore();
|
||||
}
|
||||
if (low) {
|
||||
fill.setColor(0x33000000);
|
||||
c.drawCircle(cx - 36f * u, cy - 30f * u, 13f * u, fill);
|
||||
c.drawCircle(cx + 34f * u, cy + 40f * u, 17f * u, fill);
|
||||
c.drawCircle(cx + 40f * u, cy - 36f * u, 8f * u, fill);
|
||||
}
|
||||
line.setColor(low ? MOON : SUN_LIGHT);
|
||||
line.setStrokeWidth(4f * u);
|
||||
c.drawCircle(cx, cy, rad, line);
|
||||
String number = fmt(v.socPercent, "%.0f %%", "—");
|
||||
outlined(c, number, cx, cy + 20f * u, 58f * u, BONE, serif, Paint.Align.CENTER, u);
|
||||
if (low && blinkOn) outlined(c, "BATTERY LOW", cx, cy + 56f * u, 21f * u, BONE, face, Paint.Align.CENTER, u);
|
||||
else if (!low) outlined(c, "BATTERY", cx, cy + 56f * u, 19f * u, YELLOW, face, Paint.Align.CENTER, u);
|
||||
}
|
||||
|
||||
// ---------- The leaf: trip, usage, climate, odometer ----------
|
||||
|
||||
private void drawLeaf(Canvas c, float u) {
|
||||
float l = 20f * u, rr = 336f * u, t = 34f * u, b = 246f * u;
|
||||
// A stem, then the leaf: a rounded sheet with a pointed tip and veins.
|
||||
line.setColor(GREEN_DARK);
|
||||
line.setStrokeWidth(7f * u);
|
||||
c.drawLine(l + 6f * u, b - 4f * u, l - 12f * u, b + 20f * u, line);
|
||||
path.reset();
|
||||
path.moveTo(l + 30f * u, t);
|
||||
path.lineTo(rr - 50f * u, t);
|
||||
path.quadTo(rr, t + 4f * u, rr + 10f * u, (t + b) / 2f);
|
||||
path.quadTo(rr, b - 4f * u, rr - 50f * u, b);
|
||||
path.lineTo(l + 30f * u, b);
|
||||
path.quadTo(l, b, l, b - 30f * u);
|
||||
path.lineTo(l, t + 30f * u);
|
||||
path.quadTo(l, t, l + 30f * u, t);
|
||||
path.close();
|
||||
fill.setColor(LEAF);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(0xFF2E8B3A);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawPath(path, line);
|
||||
line.setColor(0x55B6F0A8);
|
||||
line.setStrokeWidth(2.5f * u);
|
||||
c.drawLine(l + 8f * u, (t + b) / 2f, rr + 6f * u, (t + b) / 2f, line);
|
||||
for (int i = 1; i < 6; i++) {
|
||||
float x = l + i * 50f * u;
|
||||
c.drawLine(x, (t + b) / 2f, x + 26f * u, t + 8f * u, line);
|
||||
c.drawLine(x, (t + b) / 2f, x + 26f * u, b - 8f * u, line);
|
||||
}
|
||||
String[][] rows = {
|
||||
{ "TRIP", fmt(v.tripKm, "%.1f km", "—") },
|
||||
{ "5 MIN", fmt(v.usage5Min, "%.1f kWh/100", "—") },
|
||||
{ "SINCE START", fmt(v.usageTrip, "%.1f kWh/100", "—") },
|
||||
{ "CLIMATE", fmt(v.climateCelsius, "%.0f °C", "—") },
|
||||
{ "ODOMETER", fmt(v.odometerKm, "%,.0f km", "—") },
|
||||
};
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
float top = t + 12f * u + i * 38f * u;
|
||||
c.save();
|
||||
c.rotate((i % 2 == 0 ? -0.6f : 0.6f), (l + rr) / 2f, top + 16f * u);
|
||||
r.set(l + 14f * u, top, rr - 14f * u, top + 32f * u);
|
||||
fill.setColor(PAPER);
|
||||
c.drawRect(r, fill);
|
||||
label(c, rows[i][0], l + 24f * u, top + 22f * u, 16f * u, 0xFF6B4A8A, thin, Paint.Align.LEFT);
|
||||
label(c, rows[i][1], rr - 24f * u, top + 24f * u, 21f * u, INK, face, Paint.Align.RIGHT);
|
||||
c.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The clock ----------
|
||||
|
||||
private void drawClock(Canvas c, int w, float u) {
|
||||
float cx = w - 115f * u, cy = 96f * u, rad = 62f * u;
|
||||
// Two leaves on top.
|
||||
fill.setColor(LEAF);
|
||||
for (int side = -1; side <= 1; side += 2) {
|
||||
c.save();
|
||||
c.rotate(side * 40f, cx, cy);
|
||||
r.set(cx - 12f * u, cy - rad - 34f * u, cx + 12f * u, cy - rad + 8f * u);
|
||||
c.drawOval(r, fill);
|
||||
c.restore();
|
||||
}
|
||||
fill.setColor(PAPER);
|
||||
c.drawCircle(cx, cy, rad, fill);
|
||||
line.setColor(GREEN_DARK);
|
||||
line.setStrokeWidth(6f * u);
|
||||
c.drawCircle(cx, cy, rad, line);
|
||||
label(c, v.clock.isEmpty() ? "—" : v.clock, cx, cy + 14f * u, 40f * u, INK, serif, Paint.Align.CENTER);
|
||||
}
|
||||
|
||||
// ---------- The fruit: the range ----------
|
||||
|
||||
private void drawFruit(Canvas c, int w, float u) {
|
||||
float rr = w - 40f * u, l = rr - 250f * u;
|
||||
label(c, fmt(v.rangeKm, "%.0f km", "— km"), (l + rr) / 2f, 220f * u, 36f * u, BONE, serif, Paint.Align.CENTER);
|
||||
int whole = Tally.lit(v.rangeKm, 50f, FRUIT);
|
||||
int index = 0;
|
||||
for (int row = 0; row < 5; row++) {
|
||||
for (int k = 0; k <= row; k++, index++) {
|
||||
fruit(c, row, l + 24f * u + k * 46f * u, 256f * u + row * 50f * u, 19f * u, index < whole, u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** One fruit of kind {@code kind} (apple, pear, plum, strawberry, orange); used up ones have a bite out. */
|
||||
private void fruit(Canvas c, int kind, float x, float y, float s, boolean whole, float u) {
|
||||
final int[] colours = { 0xFFE53935, 0xFFA8D83F, 0xFF7E3FBF, 0xFFE8263E, 0xFFFF8F1F };
|
||||
path.reset();
|
||||
switch (kind) {
|
||||
case 1: r.set(x - s * 0.8f, y - s, x + s * 0.8f, y + s); path.addOval(r, Path.Direction.CW); break;
|
||||
case 3:
|
||||
path.moveTo(x - s, y - s * 0.6f);
|
||||
path.lineTo(x + s, y - s * 0.6f);
|
||||
path.lineTo(x, y + s);
|
||||
path.close();
|
||||
break;
|
||||
default: path.addCircle(x, y, s, Path.Direction.CW);
|
||||
}
|
||||
if (!whole) {
|
||||
bite.reset();
|
||||
bite.addCircle(x + s * 0.85f, y - s * 0.55f, s * 0.5f, Path.Direction.CW);
|
||||
path.op(bite, Path.Op.DIFFERENCE);
|
||||
}
|
||||
fill.setColor(whole ? colours[kind] : AccentColor.withAlpha(colours[kind], 0x78));
|
||||
c.drawPath(path, fill);
|
||||
if (!whole) return;
|
||||
// A stem and a leaf, seeds on the strawberry.
|
||||
line.setColor(kind == 3 ? GREEN_DARK : 0xFF6B4226);
|
||||
line.setStrokeWidth(2.5f * u);
|
||||
c.drawLine(x, y - s * (kind == 3 ? 0.6f : 0.95f), x + 3f * u, y - s * 1.3f, line);
|
||||
fill.setColor(GREEN);
|
||||
r.set(x + 1f * u, y - s * 1.35f, x + 11f * u, y - s * 1.05f);
|
||||
c.drawOval(r, fill);
|
||||
if (kind == 3) {
|
||||
fill.setColor(YELLOW);
|
||||
for (int i = 0; i < 3; i++) c.drawCircle(x + (i - 1) * 7f * u, y + (i % 2) * 7f * u - 2f * u, 1.6f * u, fill);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The stages of its life: the gears ----------
|
||||
|
||||
private void drawGears(Canvas c, int w, float u) {
|
||||
float rr = w - 40f * u, l = rr - 250f * u, cy = 520f * u, rad = 26f * u;
|
||||
String[] gears = { "P", "R", "N", "D" };
|
||||
String now = Gear.label(v.gear);
|
||||
float step = (rr - l) / 4f;
|
||||
for (int i = 0; i < gears.length; i++) {
|
||||
float mid = l + (i + 0.5f) * step;
|
||||
boolean on = gears[i].equals(now);
|
||||
if (on) {
|
||||
fill.setColor(0x55FFE066);
|
||||
c.drawCircle(mid, cy, 40f * u, fill);
|
||||
}
|
||||
fill.setColor(0xFF1B2A4A);
|
||||
c.drawCircle(mid, cy, rad, fill);
|
||||
line.setColor(on ? YELLOW : 0xFF3A5080);
|
||||
line.setStrokeWidth(2.5f * u);
|
||||
c.drawCircle(mid, cy, rad, line);
|
||||
stage(c, i, mid, cy, u, on);
|
||||
label(c, gears[i], mid, cy + rad + 22f * u, 22f * u, on ? YELLOW : 0xB0FFFFFF, serif, Paint.Align.CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
/** The life stage for gear {@code i}: an egg, the caterpillar, a cocoon, a butterfly. */
|
||||
private void stage(Canvas c, int i, float x, float y, float u, boolean on) {
|
||||
int alpha = on ? 0xFF : 0x90;
|
||||
switch (i) {
|
||||
case 0:
|
||||
r.set(x - 9f * u, y - 13f * u, x + 9f * u, y + 13f * u);
|
||||
fill.setColor(AccentColor.withAlpha(0xFFF6F0E4, alpha));
|
||||
c.drawOval(r, fill);
|
||||
break;
|
||||
case 1:
|
||||
for (int k = 0; k < 3; k++) {
|
||||
fill.setColor(AccentColor.withAlpha(k == 2 ? HEAD : GREEN, alpha));
|
||||
c.drawCircle(x + (k - 1) * 10f * u, y + 2f * u, 7f * u, fill);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
r.set(x - 8f * u, y - 16f * u, x + 8f * u, y + 16f * u);
|
||||
fill.setColor(AccentColor.withAlpha(0xFFB08A5A, alpha));
|
||||
c.drawOval(r, fill);
|
||||
line.setColor(AccentColor.withAlpha(0xFF6B4226, alpha));
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawLine(x - 7f * u, y - 4f * u, x + 7f * u, y - 4f * u, line);
|
||||
c.drawLine(x - 7f * u, y + 5f * u, x + 7f * u, y + 5f * u, line);
|
||||
break;
|
||||
default:
|
||||
fill.setColor(AccentColor.withAlpha(0xFF7E57E0, alpha));
|
||||
r.set(x - 16f * u, y - 14f * u, x, y + 2f * u);
|
||||
c.drawOval(r, fill);
|
||||
r.set(x, y - 14f * u, x + 16f * u, y + 2f * u);
|
||||
c.drawOval(r, fill);
|
||||
fill.setColor(AccentColor.withAlpha(0xFFFF5FA2, alpha));
|
||||
r.set(x - 14f * u, y - 2f * u, x, y + 12f * u);
|
||||
c.drawOval(r, fill);
|
||||
r.set(x, y - 2f * u, x + 14f * u, y + 12f * u);
|
||||
c.drawOval(r, fill);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The branch it crawls on ----------
|
||||
|
||||
/** A branch under the caterpillar; its knots and leaves slide backwards as the car goes. */
|
||||
private void drawBranch(Canvas c, int w, float u) {
|
||||
float hx = w / 2f + 250f * u, y = 410f * u + 87f * u;
|
||||
float l = hx - 640f * u, rr = hx + 110f * u, span = rr - l;
|
||||
line.setColor(0xFF6B4A2B);
|
||||
line.setStrokeWidth(20f * u);
|
||||
c.drawLine(l, y, rr, y, line);
|
||||
line.setColor(0x44FFD59A);
|
||||
line.setStrokeWidth(4f * u);
|
||||
c.drawLine(l + 6f * u, y - 6f * u, rr - 6f * u, y - 6f * u, line);
|
||||
for (int k = 0; k < 8; k++) {
|
||||
float x = l + (((k * span / 8f) - travel) % span + span) % span;
|
||||
fill.setColor(0xFF4A3018);
|
||||
r.set(x - 9f * u, y - 5f * u, x + 9f * u, y + 6f * u);
|
||||
c.drawOval(r, fill);
|
||||
if (k % 2 == 0) {
|
||||
fill.setColor(0xFF3FA34D);
|
||||
r.set(x - 4f * u, y + 6f * u, x + 20f * u, y + 18f * u);
|
||||
c.drawOval(r, fill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The caterpillar: speed and power ----------
|
||||
|
||||
/** Paper for a segment: three base colours, a light and a teal for the strokes; by state. */
|
||||
private static final int[][] DRAWING = { { 0xFF5FBF52, 0xFF2E9E6A, 0xFF8ACD4A }, { 0xFFC9F06A, 0xFF1F8A7A } };
|
||||
private static final int[][] REGEN = { { 0xFF2A8FC9, 0xFF1F78B4, 0xFF4AA8DA }, { 0xFF9BDCFF, 0xFF3AC8D0 } };
|
||||
private static final int[][] OFF = { { 0xFF2B7A40, 0xFF256B38, 0xFF328C4B }, { 0xFF3A9060, 0xFF2A8078 } };
|
||||
private static final float SEG_R = 47f;
|
||||
|
||||
/** [state][base colour]: a round, lit-from-the-upper-left shading for one segment, in its own units. */
|
||||
private final Paint[][] shades = new Paint[3][3];
|
||||
private Paint headShade;
|
||||
private float shadesFor;
|
||||
|
||||
private static int mix(int a, int b, float t) {
|
||||
int ar = (a >> 16) & 0xFF, ag = (a >> 8) & 0xFF, ab = a & 0xFF;
|
||||
int br = (b >> 16) & 0xFF, bg = (b >> 8) & 0xFF, bb = b & 0xFF;
|
||||
return 0xFF000000 | (ar + (int) ((br - ar) * t)) << 16 | (ag + (int) ((bg - ag) * t)) << 8 | (ab + (int) ((bb - ab) * t));
|
||||
}
|
||||
|
||||
private void buildShades(float u) {
|
||||
int[][][] states = { DRAWING, REGEN, OFF };
|
||||
float r = SEG_R * u;
|
||||
for (int st = 0; st < 3; st++) {
|
||||
for (int k = 0; k < 3; k++) {
|
||||
int base = states[st][0][k];
|
||||
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
p.setShader(new RadialGradient(-0.3f * r, -0.35f * r, 1.5f * r,
|
||||
new int[] { mix(base, 0xFFFFFFFF, 0.3f), base, mix(base, 0xFF000000, 0.18f) },
|
||||
new float[] { 0f, 0.55f, 1f }, Shader.TileMode.CLAMP));
|
||||
shades[st][k] = p;
|
||||
}
|
||||
}
|
||||
headShade = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
headShade.setShader(new RadialGradient(-25f * u, -30f * u, 130f * u,
|
||||
new int[] { 0xFFFF6A45, 0xFFE8452B, 0xFFA82414 }, new float[] { 0f, 0.55f, 1f }, Shader.TileMode.CLAMP));
|
||||
shadesFor = u;
|
||||
}
|
||||
|
||||
/**
|
||||
* The caterpillar, after the picture book's newer pictures: plump round segments like balls of
|
||||
* wound paper, each with brush strokes in light green and teal, bigger towards the head, a fringe
|
||||
* of yellow and orange bristles along the back, stubby brown feet, and a red apple-shaped head with
|
||||
* big teal eyes and two lilac ears. At rest it arches high in the middle. Power lights the segments
|
||||
* (bright green, or blue when regenerating, dull when dark); the speed is on the head under the
|
||||
* eyes. Crawling, a real caterpillar pulls its rear forward and humps the body up, and the hump
|
||||
* runs from the tail to the head while the feet it lifts swing forward and the others stay planted;
|
||||
* the body also bunches and stretches as it goes.
|
||||
*/
|
||||
private void drawCaterpillar(Canvas c, int w, float u, float pulse) {
|
||||
if (shadesFor != u) buildShades(u);
|
||||
float hx = w / 2f + 250f * u, hy = 410f * u;
|
||||
int lit = Tally.ofPower(v.powerKw, 150f, 50f, SEGMENTS);
|
||||
int state = lit == 0 ? 2 : regen() ? 1 : 0;
|
||||
float arch = (80f * (1f - crawl) + 26f * crawl) * u;
|
||||
float cycle = phase % 1f;
|
||||
float stretch = 1f + 0.08f * crawl * (float) Math.sin(cycle * 2f * Math.PI);
|
||||
float wave = 1.25f - cycle * 1.6f;
|
||||
float[] xs = new float[SEGMENTS], ys = new float[SEGMENTS], lift = new float[SEGMENTS];
|
||||
for (int i = 0; i < SEGMENTS; i++) {
|
||||
float along = (SEGMENTS - 1 - i) / (SEGMENTS - 1f);
|
||||
float d = Math.max(0f, 1f - Math.abs(along - wave) / 0.4f);
|
||||
lift[i] = crawl * d * d * (3f - 2f * d);
|
||||
xs[i] = hx - 104f * u - i * 46f * u * stretch;
|
||||
ys[i] = hy + 22f * u - arch * (float) Math.sin(Math.PI * (i + 0.7) / (SEGMENTS + 0.4)) - lift[i] * 46f * u;
|
||||
}
|
||||
float r = SEG_R * u;
|
||||
// From the tail towards the head, each overlapping the one before.
|
||||
for (int i = SEGMENTS - 1; i >= 0; i--) {
|
||||
boolean on = i < lit;
|
||||
int[][] pal = on ? (regen() ? REGEN : DRAWING) : OFF;
|
||||
Paint shade = shades[on ? (regen() ? 1 : 0) : 2][i % 3];
|
||||
int prev = Math.max(0, i - 1), next = Math.min(SEGMENTS - 1, i + 1);
|
||||
float angle = (float) Math.toDegrees(Math.atan2(ys[prev] - ys[next], xs[prev] - xs[next]));
|
||||
// Bigger towards the head, and swelling a little with the music.
|
||||
float grow = (0.82f + 0.2f * (1f - i / (SEGMENTS - 1f))) * (1f + (on ? pulse * (i % 2 == 0 ? 0.14f : 0.09f) : 0f));
|
||||
c.save();
|
||||
c.translate(xs[i], ys[i]);
|
||||
c.rotate(angle);
|
||||
c.scale(grow, grow);
|
||||
// A stubby brown foot under every other one: a lifted foot swings forward and up.
|
||||
if (i % 2 == 0) {
|
||||
float up = lift[i] * 12f * u, fwd = lift[i] * 12f * u;
|
||||
this.r.set(-0.2f * r + fwd, 0.85f * r - up, 0.4f * r + fwd, 1.4f * r - up);
|
||||
fill.setColor(0xFF9A6A3A);
|
||||
c.drawOval(this.r, fill);
|
||||
}
|
||||
this.r.set(-r, -1.05f * r, r, 1.05f * r);
|
||||
c.drawOval(this.r, shade);
|
||||
// Brush strokes down the ball, light and teal in turn.
|
||||
line.setStrokeWidth(3f * u);
|
||||
for (int k = -2; k <= 2; k++) {
|
||||
float x0 = k * 0.32f * r;
|
||||
line.setColor(AccentColor.withAlpha(pal[1][(k + 2) % 2], 0x70));
|
||||
path.reset();
|
||||
path.moveTo(x0 * 0.7f, -0.8f * r);
|
||||
path.quadTo(x0 * 1.5f, 0f, x0 * 0.7f, 0.8f * r);
|
||||
c.drawPath(path, line);
|
||||
}
|
||||
// The fringe along the back, in yellow and orange.
|
||||
line.setStrokeWidth(3f * u);
|
||||
for (int k = -3; k <= 3; k++) {
|
||||
line.setColor(on ? (k % 2 == 0 ? 0xFFFFC21F : 0xFFFF8A1F) : 0x80FFC21F);
|
||||
c.drawLine(k * 0.28f * r, (-0.98f + Math.abs(k) * 0.05f) * r, k * 0.34f * r, (-1.22f + Math.abs(k) * 0.06f) * r, line);
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
drawHead(c, hx, hy, u, pulse);
|
||||
boolean regen = regen();
|
||||
String power = v.powerKw == null ? "— kW" : String.format(Locale.US, regen ? "+%.1f kW" : "%.1f kW",
|
||||
Math.abs(v.powerKw));
|
||||
outlined(c, power, hx - 104f * u - 4.5f * 46f * u, 556f * u, 34f * u, regen ? ICE : BONE, face, Paint.Align.CENTER, u);
|
||||
}
|
||||
|
||||
/** A red apple-shaped head, big teal eyes with yellow rims and two lilac ears, the speed under the eyes. */
|
||||
private void drawHead(Canvas c, float hx, float hy, float u, float pulse) {
|
||||
float hs = 1f + pulse * 0.04f;
|
||||
c.save();
|
||||
c.translate(hx, hy);
|
||||
c.scale(hs, hs);
|
||||
// The ears, stubby and rounded, leaning out.
|
||||
for (int side = -1; side <= 1; side += 2) {
|
||||
c.save();
|
||||
c.rotate(side * 12f, side * 36f * u, -64f * u);
|
||||
r.set(side * 36f * u - 15f * u, -122f * u, side * 36f * u + 15f * u, -58f * u);
|
||||
fill.setColor(0xFFB59AE0);
|
||||
c.drawOval(r, fill);
|
||||
r.set(side * 36f * u - 15f * u, -122f * u, side * 36f * u - 2f * u, -58f * u);
|
||||
fill.setColor(0x40FFFFFF);
|
||||
c.drawOval(r, fill);
|
||||
c.restore();
|
||||
}
|
||||
r.set(-88f * u, -76f * u, 88f * u, 82f * u);
|
||||
c.drawOval(r, headShade);
|
||||
fill.setColor(0x55000000);
|
||||
r.set(-12f * u, -78f * u, 12f * u, -68f * u);
|
||||
c.drawOval(r, fill);
|
||||
for (int side = -1; side <= 1; side += 2) {
|
||||
float ex = side * 34f * u;
|
||||
r.set(ex - 19f * u, -50f * u, ex + 19f * u, 6f * u);
|
||||
fill.setColor(0xFF2FA89A);
|
||||
c.drawOval(r, fill);
|
||||
line.setColor(0xFFFFD84A);
|
||||
line.setStrokeWidth(4f * u);
|
||||
c.drawOval(r, line);
|
||||
fill.setColor(0x99FFFFFF);
|
||||
c.drawCircle(ex - 6f * u, -34f * u, 4.5f * u, fill);
|
||||
}
|
||||
outlined(c, fmt(v.speedKmh, "%.0f", "—"), 0f, 50f * u, 40f * u, 0xFFFFF3D0, serif, Paint.Align.CENTER, u);
|
||||
label(c, "KM/H", 0f, 70f * u, 12f * u, 0xFFFFE0B0, face, Paint.Align.CENTER);
|
||||
c.restore();
|
||||
}
|
||||
|
||||
// ---------- What plays: a butterfly or a ladybird tows it on tissue paper ----------
|
||||
|
||||
/**
|
||||
* A butterfly and a ladybird take turns towing a waving strip of tissue paper with the song across
|
||||
* the sky, behind the clock and the leaf, again and again while something plays. The paper's
|
||||
* colour is fixed by the song. Returns whether it is drawn, so the view keeps animating for it.
|
||||
*/
|
||||
private boolean drawSong(Canvas c, int w, float u) {
|
||||
if (v.songTitle == null) return false;
|
||||
text.setTypeface(face);
|
||||
text.setTextSize(19f * u);
|
||||
text.setStyle(Paint.Style.FILL);
|
||||
String s = SongBanner.line(text, v.songTitle, v.songArtist, Math.min(560f * u, w * 0.45f));
|
||||
float t = SystemClock.uptimeMillis() / 1000f;
|
||||
float bw = text.measureText(s) + 40f * u, bh = 28f * u, tow = 76f * u;
|
||||
float bx = SongBanner.left(t, w, bw, tow, u);
|
||||
float yc = 66f * u + (float) Math.sin(t * 1.3f) * 3f * u;
|
||||
int paper = TISSUE[Math.floorMod((v.songTitle + "\n" + v.songArtist).hashCode(), TISSUE.length)];
|
||||
boolean darkInk = paper == TISSUE[3] || paper == TISSUE[5];
|
||||
banner.draw(c, fill, line, text, s, bx, yc, bw, bh, t, u, 9f * u, paper, 0x66FFFFFF, darkInk ? INK : 0xFFFFFFFF);
|
||||
boolean butterfly = SongBanner.pass(t, w, bw, tow, u) % 2 == 0;
|
||||
float right = bx + bw, cx = right + tow, cy = yc - 4f * u, size = (butterfly ? 60f : 44f) * u;
|
||||
float[] tail = butterfly ? SongCarriers.butterflyTail : SongCarriers.ladybirdTail;
|
||||
float ax = cx + tail[0] * size, ay = cy + tail[1] * size;
|
||||
line.setColor(0xCCFFFFFF);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawLine(right, yc - bh / 2f + 3f * u, ax, ay, line);
|
||||
c.drawLine(right, yc + bh / 2f - 3f * u, ax, ay, line);
|
||||
float flap = (float) Math.sin(t * (butterfly ? 7f : 18f));
|
||||
if (butterfly) SongCarriers.butterfly(c, fill, line, path, r, cx, cy, size, flap, u);
|
||||
else SongCarriers.ladybird(c, fill, line, path, r, cx, cy, size, flap, u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.DashPathEffect;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RadialGradient;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The Christmas Overview style: a snowy village night, warm and festive.
|
||||
*
|
||||
* In the middle a Christmas tree with a big red bauble in front of it: the speed. Fairy lights
|
||||
* arch over the tree: the power, left to right in warm gold while drawing, ice blue and right to
|
||||
* left while regenerating. On the left a glass mug of hot cocoa filling with the charge, cold blue
|
||||
* with "BATTERY LOW" blinking below 20 %, under gift tags with trip, usage, climate and odometer.
|
||||
* On the right a wreath holds the clock, and a snowy signboard reads the range, with a candy cane
|
||||
* per 50 km; P R N D are four baubles, the one engaged lit. The snow drift along the bottom is the
|
||||
* last two minutes of power, rising with it, in gold or, where it regenerated, ice blue. Unread
|
||||
* values show "—" and light nothing. With music playing, the tree's lights and the fairy lights
|
||||
* flare on the beat ({@link MusicTap}); with no music they twinkle as before. What plays crosses
|
||||
* the sky on a banner of wrapping paper, towed by Santa's sleigh and, on the next pass, an angel
|
||||
* ({@link NowPlaying}).
|
||||
*
|
||||
* The night, the hills and the stars are drawn once into a bitmap per size; only the rest goes over
|
||||
* it. Snow falls and the lights twinkle (about 30 frames a second) only while the car moves and the
|
||||
* view is on screen; the lights' pulse and the song keep it drawing while there are any.
|
||||
*/
|
||||
final class ChristmasView extends ClusterCanvasView {
|
||||
|
||||
private static final int BULBS = 25, FLAKES = 70, CANES = 8;
|
||||
private static final int SKY_TOP = 0xFF06142B, SKY_LOW = 0xFF173A5E, SKY_END = 0xFF2A5A7A;
|
||||
private static final int SNOW = 0xFFE9F2FF, PINE = 0xFF1F7A3C, PINE_DARK = 0xFF0F4A24;
|
||||
private static final int RED = 0xFFE53946, GOLD = 0xFFFFD04A, BONE = 0xFFF6F0E4, INK = 0xFF1B0F12;
|
||||
private static final int COCOA = 0xFF7B3F22, COLD = 0xFF4A8AD0, ICE = 0xFF8FD8FF, WOOD = 0xFF4A2E1A;
|
||||
private static final int[] LIGHTS = { RED, GOLD, ICE, 0xFFFFFFFF, 0xFF5CE08A };
|
||||
private static final int[] PAPER = { 0xFFC62828, 0xFF1E7B3C, 0xFF2B5FA8, 0xFFD9A62E, 0xFFF1F1F1 };
|
||||
|
||||
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint image = new Paint(Paint.FILTER_BITMAP_FLAG);
|
||||
private final Paint glow = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path path = new Path();
|
||||
private final SongBanner banner = new SongBanner();
|
||||
private final RectF r = new RectF();
|
||||
private final Typeface serif = Typeface.create("serif", Typeface.BOLD);
|
||||
private final Typeface face = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
||||
private final Typeface thin = Typeface.create("sans-serif-condensed", Typeface.NORMAL);
|
||||
|
||||
private Bitmap sky;
|
||||
private int skyW, skyH;
|
||||
private float time;
|
||||
private long lastFrame;
|
||||
private MusicTap music;
|
||||
|
||||
ChristmasView(Context context) {
|
||||
super(context);
|
||||
line.setStyle(Paint.Style.STROKE);
|
||||
line.setStrokeCap(Paint.Cap.ROUND);
|
||||
}
|
||||
|
||||
/** Where the lights hear the music; without it, they do not follow any. */
|
||||
void setMusic(MusicTap tap) {
|
||||
music = tap;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
sky = null;
|
||||
skyW = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas c) {
|
||||
super.onDraw(c);
|
||||
int w = getWidth(), h = getHeight();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
float u = Math.min(h / 640f, w / 1100f);
|
||||
boolean moving = v.speedKmh != null && v.speedKmh > 0.5f;
|
||||
if (moving) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
time += lastFrame == 0 ? 0f : Math.min(0.2f, (now - lastFrame) / 1000f);
|
||||
lastFrame = now;
|
||||
} else {
|
||||
lastFrame = 0;
|
||||
}
|
||||
if (sky == null || skyW != w || skyH != h) buildSky(w, h, u);
|
||||
c.drawBitmap(sky, 0, 0, image);
|
||||
boolean song = drawSong(c, w, u);
|
||||
drawSnow(c, w, h, u);
|
||||
drawDrift(c, w, h, u);
|
||||
float pulse = music == null ? 0f : music.pulse();
|
||||
drawTree(c, w, u, pulse);
|
||||
drawArch(c, w, u, pulse);
|
||||
drawBauble(c, w, u, pulse);
|
||||
drawWreath(c, w, u);
|
||||
drawTags(c, u);
|
||||
drawMug(c, u);
|
||||
drawBoard(c, w, u);
|
||||
drawGears(c, w, u);
|
||||
keepAnimating(moving || pulse > 0f || song, 33);
|
||||
}
|
||||
|
||||
private void label(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a) {
|
||||
text.setTypeface(tf);
|
||||
text.setTextSize(size);
|
||||
text.setTextAlign(a);
|
||||
text.setColor(colour);
|
||||
c.drawText(s, x, y, text);
|
||||
}
|
||||
|
||||
/** Text with a dark outline, to read over whatever is behind it. */
|
||||
private void outlined(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a, float u) {
|
||||
text.setStyle(Paint.Style.STROKE);
|
||||
text.setStrokeWidth(Math.max(3f * u, size * 0.12f));
|
||||
text.setStrokeJoin(Paint.Join.ROUND);
|
||||
label(c, s, x, y, size, 0xDD0A0608, tf, a);
|
||||
text.setStyle(Paint.Style.FILL);
|
||||
label(c, s, x, y, size, colour, tf, a);
|
||||
}
|
||||
|
||||
private boolean regen() {
|
||||
return v.powerKw != null && v.powerKw < -0.05f;
|
||||
}
|
||||
|
||||
// ---------- Night sky and hills ----------
|
||||
|
||||
private void buildSky(int w, int h, float u) {
|
||||
sky = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
||||
skyW = w;
|
||||
skyH = h;
|
||||
Canvas c = new Canvas(sky);
|
||||
Paint p = new Paint();
|
||||
p.setShader(new LinearGradient(0, 0, 0, h, new int[] { SKY_TOP, SKY_LOW, SKY_END },
|
||||
new float[] { 0f, 0.7f, 1f }, Shader.TileMode.CLAMP));
|
||||
c.drawRect(0, 0, w, h, p);
|
||||
Paint star = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
star.setColor(0x99FFFFFF);
|
||||
for (int i = 0; i < 90; i++) {
|
||||
float x = ((i * 191 + 17) % 1009) / 1009f * w, y = ((i * 83 + 5) % 421) * u;
|
||||
c.drawCircle(x, y, (i % 4 == 0 ? 1.7f : 0.9f) * u, star);
|
||||
}
|
||||
// Two ranges of snowy hills, with pines on the far one.
|
||||
hills(c, w, h, h - 150f * u, 0xFF28496B, 0.8f, u);
|
||||
Paint pine = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
pine.setColor(0xFF0F3A2A);
|
||||
for (int i = 0; i < 14; i++) {
|
||||
float x = ((i * 233 + 41) % 997) / 997f * w, tall = (36f + (i * 17) % 30) * u;
|
||||
float base = h - 150f * u + 8f * u;
|
||||
Path tri = new Path();
|
||||
tri.moveTo(x, base - tall);
|
||||
tri.lineTo(x + tall * 0.32f, base);
|
||||
tri.lineTo(x - tall * 0.32f, base);
|
||||
tri.close();
|
||||
c.drawPath(tri, pine);
|
||||
}
|
||||
hills(c, w, h, h - 100f * u, 0xFF3C6286, 1.5f, u);
|
||||
}
|
||||
|
||||
private void hills(Canvas c, int w, int h, float base, int colour, float wave, float u) {
|
||||
Path hill = new Path();
|
||||
hill.moveTo(0, h);
|
||||
for (int i = 0; i <= 24; i++) {
|
||||
float x = w * i / 24f;
|
||||
hill.lineTo(x, base - (float) (Math.sin(i * wave) * 0.5 + Math.sin(i * wave * 0.37 + 1) * 0.5) * 22f * u);
|
||||
}
|
||||
hill.lineTo(w, h);
|
||||
hill.close();
|
||||
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
p.setColor(colour);
|
||||
c.drawPath(hill, p);
|
||||
}
|
||||
|
||||
private void drawSnow(Canvas c, int w, int h, float u) {
|
||||
for (int i = 0; i < FLAKES; i++) {
|
||||
float fall = (30f + (i % 5) * 12f) * u;
|
||||
float y = (((i * 53) % 600) * u + time * fall) % h;
|
||||
float x = ((i * 97 + 13) % 1000) / 1000f * w + (float) Math.sin(time * 0.8f + i) * 12f * u;
|
||||
fill.setColor(i % 3 == 0 ? 0xE0FFFFFF : 0x90FFFFFF);
|
||||
c.drawCircle(x, y, (1f + (i % 3) * 0.8f) * u, fill);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The drift: the last two minutes of power ----------
|
||||
|
||||
private void drawDrift(Canvas c, int w, int h, float u) {
|
||||
float base = h - 44f * u, amp = 36f * u;
|
||||
float[] trail = v.powerTrail;
|
||||
path.reset();
|
||||
path.moveTo(0, h);
|
||||
int n = trail == null ? 0 : trail.length;
|
||||
if (n == 0) {
|
||||
path.lineTo(0, base);
|
||||
path.lineTo(w, base);
|
||||
} else {
|
||||
for (int i = 0; i < n; i++) {
|
||||
float p = trail[i];
|
||||
float height = Float.isNaN(p) ? 0f : Math.min(1f, Math.abs(p) / (p < 0 ? 40f : 60f));
|
||||
path.lineTo(i / (n - 1f) * w, base - height * amp);
|
||||
}
|
||||
}
|
||||
path.lineTo(w, h);
|
||||
path.close();
|
||||
fill.setColor(SNOW);
|
||||
c.drawPath(path, fill);
|
||||
if (n > 1) {
|
||||
line.setStrokeWidth(3f * u);
|
||||
for (int i = 1; i < n; i++) {
|
||||
float a = trail[i - 1], b = trail[i];
|
||||
if (Float.isNaN(a) || Float.isNaN(b)) continue;
|
||||
float ha = Math.min(1f, Math.abs(a) / (a < 0 ? 40f : 60f));
|
||||
float hb = Math.min(1f, Math.abs(b) / (b < 0 ? 40f : 60f));
|
||||
line.setColor(b < 0 ? ICE : GOLD);
|
||||
c.drawLine((i - 1f) / (n - 1f) * w, base - ha * amp, i / (n - 1f) * w, base - hb * amp, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The tree ----------
|
||||
|
||||
private void drawTree(Canvas c, int w, float u, float pulse) {
|
||||
float cx = w / 2f;
|
||||
// The trunk, then three tiers, each with a darker underside.
|
||||
r.set(cx - 22f * u, 480f * u, cx + 22f * u, 526f * u);
|
||||
fill.setColor(WOOD);
|
||||
c.drawRect(r, fill);
|
||||
tier(c, cx, 118f * u, 238f * u, 96f * u, u);
|
||||
tier(c, cx, 190f * u, 358f * u, 136f * u, u);
|
||||
tier(c, cx, 270f * u, 492f * u, 182f * u, u);
|
||||
// The star on top, flaring with the music.
|
||||
float sy = 104f * u, flare = 1f + pulse * 0.5f;
|
||||
glow.setShader(new RadialGradient(cx, sy, 60f * u * flare,
|
||||
new int[] { AccentColor.withAlpha(GOLD, 0x55 + (int) (pulse * 0x80)), AccentColor.withAlpha(GOLD, 0) }, null,
|
||||
Shader.TileMode.CLAMP));
|
||||
c.drawCircle(cx, sy, 60f * u * flare, glow);
|
||||
path.reset();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
double a = -Math.PI / 2 + Math.PI * i / 5;
|
||||
float rad = (i % 2 == 0 ? 24f : 10f) * u;
|
||||
float x = cx + (float) Math.cos(a) * rad, y = sy + (float) Math.sin(a) * rad;
|
||||
if (i == 0) path.moveTo(x, y);
|
||||
else path.lineTo(x, y);
|
||||
}
|
||||
path.close();
|
||||
fill.setColor(GOLD);
|
||||
c.drawPath(path, fill);
|
||||
// The lights: strung along each tier's lower edge, steady, twinkling a little while driving,
|
||||
// flaring together on the beat.
|
||||
float[][] strings = { { 238f, 96f, 5 }, { 358f, 136f, 7 }, { 492f, 182f, 10 } };
|
||||
int i = 0;
|
||||
for (float[] st : strings) {
|
||||
int n = (int) st[2];
|
||||
for (int k = 0; k < n; k++, i++) {
|
||||
float x = cx - st[1] * 0.85f * u + k * (1.7f * st[1] * u) / (n - 1);
|
||||
float y = (st[0] - 16f + (k % 2) * 9f) * u;
|
||||
int colour = LIGHTS[i % LIGHTS.length];
|
||||
float twinkle = 0.5f + 0.5f * (float) Math.sin(time * 3f + i * 1.7f);
|
||||
int alpha = Math.min(0xFF, 0x58 + (int) (twinkle * 0x38) + (int) (pulse * 0x90));
|
||||
fill.setColor(AccentColor.withAlpha(colour, alpha));
|
||||
c.drawCircle(x, y, (11f + pulse * 9f) * u, fill);
|
||||
fill.setColor(brighter(colour, pulse * 0.4f));
|
||||
c.drawCircle(x, y, 4.8f * u * (1f + pulse * 0.35f), fill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tier(Canvas c, float cx, float apex, float base, float half, float u) {
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
float dy = pass == 0 ? 7f * u : 0f;
|
||||
path.reset();
|
||||
path.moveTo(cx, apex + dy);
|
||||
path.lineTo(cx + half, base - 6f * u + dy);
|
||||
int scallops = 5;
|
||||
float sw = half * 2f / scallops;
|
||||
for (int i = scallops; i > 0; i--) {
|
||||
float x = cx - half + i * sw;
|
||||
path.quadTo(x - sw / 2f, base + 12f * u + dy, x - sw, base - 6f * u + dy);
|
||||
}
|
||||
path.close();
|
||||
fill.setColor(pass == 0 ? PINE_DARK : PINE);
|
||||
c.drawPath(path, fill);
|
||||
}
|
||||
}
|
||||
|
||||
/** The colour moved {@code share} of the way to white. */
|
||||
private static int brighter(int colour, float share) {
|
||||
int r = (colour >> 16) & 0xFF, g = (colour >> 8) & 0xFF, b = colour & 0xFF;
|
||||
return 0xFF000000 | (r + (int) ((255 - r) * share)) << 16 | (g + (int) ((255 - g) * share)) << 8
|
||||
| (b + (int) ((255 - b) * share));
|
||||
}
|
||||
|
||||
// ---------- The fairy lights over the tree: the power ----------
|
||||
|
||||
private void drawArch(Canvas c, int w, float u, float pulse) {
|
||||
float cx = w / 2f, cy = 348f * u, radX = 272f * u, radY = 244f * u;
|
||||
int lit = Tally.ofPower(v.powerKw, 150f, 50f, BULBS);
|
||||
boolean regen = regen();
|
||||
// The wire.
|
||||
line.setColor(PINE_DARK);
|
||||
line.setStrokeWidth(3f * u);
|
||||
path.reset();
|
||||
for (int i = 0; i <= 48; i++) {
|
||||
double a = Math.PI + Math.PI * i / 48.0;
|
||||
float x = cx + (float) Math.cos(a) * radX, y = cy + (float) Math.sin(a) * radY;
|
||||
if (i == 0) path.moveTo(x, y);
|
||||
else path.lineTo(x, y);
|
||||
}
|
||||
c.drawPath(path, line);
|
||||
for (int i = 0; i < BULBS; i++) {
|
||||
// Left to right over the top; regeneration comes back from the right.
|
||||
int k = regen ? BULBS - 1 - i : i;
|
||||
double a = Math.PI + Math.PI * k / (BULBS - 1);
|
||||
float x = cx + (float) Math.cos(a) * radX, y = cy + (float) Math.sin(a) * radY;
|
||||
boolean on = i < lit;
|
||||
if (on) {
|
||||
fill.setColor(AccentColor.withAlpha(regen ? ICE : GOLD, 0x44 + (int) (pulse * 0x50)));
|
||||
c.drawCircle(x, y, (15f + pulse * 6f) * u, fill);
|
||||
}
|
||||
fill.setColor(on ? brighter(regen ? ICE : GOLD, pulse * 0.4f) : 0x38FFFFFF);
|
||||
c.drawCircle(x, y, 7.5f * u, fill);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The big bauble: the speed ----------
|
||||
|
||||
private void drawBauble(Canvas c, int w, float u, float pulse) {
|
||||
float cx = w / 2f, cy = 372f * u, rad = 88f * u;
|
||||
glow.setShader(new RadialGradient(cx, cy, rad * (1.6f + pulse * 0.2f),
|
||||
new int[] { AccentColor.withAlpha(RED, 0x40 + (int) (pulse * 0x50)), AccentColor.withAlpha(RED, 0) }, null,
|
||||
Shader.TileMode.CLAMP));
|
||||
c.drawCircle(cx, cy, rad * (1.6f + pulse * 0.2f), glow);
|
||||
// The cap and its loop.
|
||||
line.setColor(GOLD);
|
||||
line.setStrokeWidth(3f * u);
|
||||
r.set(cx - 12f * u, cy - rad - 30f * u, cx + 12f * u, cy - rad - 6f * u);
|
||||
c.drawOval(r, line);
|
||||
glow.setShader(new RadialGradient(cx - rad * 0.3f, cy - rad * 0.35f, rad * 1.3f,
|
||||
new int[] { 0xFFFF7A82, RED, 0xFF8E1623 }, new float[] { 0f, 0.5f, 1f }, Shader.TileMode.CLAMP));
|
||||
c.drawCircle(cx, cy, rad, glow);
|
||||
r.set(cx - 16f * u, cy - rad - 12f * u, cx + 16f * u, cy - rad + 8f * u);
|
||||
fill.setColor(GOLD);
|
||||
c.drawRect(r, fill);
|
||||
// A gold band round the middle's lower half, and a highlight.
|
||||
line.setColor(0x88FFD04A);
|
||||
line.setStrokeWidth(3f * u);
|
||||
r.set(cx - rad + 6f * u, cy + 34f * u, cx + rad - 6f * u, cy + rad - 4f * u);
|
||||
c.drawArc(r, 20f, 140f, false, line);
|
||||
fill.setColor(0x55FFFFFF);
|
||||
r.set(cx - rad * 0.62f, cy - rad * 0.72f, cx - rad * 0.2f, cy - rad * 0.42f);
|
||||
c.drawOval(r, fill);
|
||||
label(c, fmt(v.speedKmh, "%.0f", "—"), cx, cy + 20f * u, 84f * u, BONE, serif, Paint.Align.CENTER);
|
||||
label(c, "KM/H", cx, cy + 50f * u, 16f * u, 0xCCFFE9EA, face, Paint.Align.CENTER);
|
||||
// The power under the tree, above the drift.
|
||||
boolean regen = regen();
|
||||
String power = v.powerKw == null ? "— kW" : String.format(Locale.US, regen ? "+%.1f kW" : "%.1f kW",
|
||||
Math.abs(v.powerKw));
|
||||
outlined(c, power, cx, 556f * u, 34f * u, regen ? ICE : BONE, face, Paint.Align.CENTER, u);
|
||||
}
|
||||
|
||||
// ---------- The wreath: the clock ----------
|
||||
|
||||
private void drawWreath(Canvas c, int w, float u) {
|
||||
float rad = 74f * u, cx = w - 100f * u - rad * 0.2f, cy = 96f * u;
|
||||
line.setColor(PINE);
|
||||
line.setStrokeWidth(24f * u);
|
||||
c.drawCircle(cx, cy, rad, line);
|
||||
line.setColor(PINE_DARK);
|
||||
line.setStrokeWidth(5f * u);
|
||||
c.drawCircle(cx, cy, rad - 8f * u, line);
|
||||
fill.setColor(0xFF2E9C52);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
double a = Math.PI * 2 * i / 16;
|
||||
c.drawCircle(cx + (float) Math.cos(a) * rad, cy + (float) Math.sin(a) * rad, 8f * u, fill);
|
||||
}
|
||||
fill.setColor(RED);
|
||||
for (int i = 0; i < 9; i++) {
|
||||
double a = Math.PI * 2 * i / 9 + 0.3;
|
||||
c.drawCircle(cx + (float) Math.cos(a) * rad, cy + (float) Math.sin(a) * rad, 4.5f * u, fill);
|
||||
}
|
||||
// The bow at the foot.
|
||||
path.reset();
|
||||
float bx = cx, by = cy + rad;
|
||||
path.moveTo(bx, by);
|
||||
path.lineTo(bx - 28f * u, by - 14f * u);
|
||||
path.lineTo(bx - 28f * u, by + 14f * u);
|
||||
path.close();
|
||||
path.moveTo(bx, by);
|
||||
path.lineTo(bx + 28f * u, by - 14f * u);
|
||||
path.lineTo(bx + 28f * u, by + 14f * u);
|
||||
path.close();
|
||||
fill.setColor(RED);
|
||||
c.drawPath(path, fill);
|
||||
c.drawCircle(bx, by, 7f * u, fill);
|
||||
// The clock face inside.
|
||||
fill.setColor(0xFFFFF6E0);
|
||||
c.drawCircle(cx, cy, rad - 18f * u, fill);
|
||||
line.setColor(GOLD);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawCircle(cx, cy, rad - 18f * u, line);
|
||||
label(c, v.clock.isEmpty() ? "—" : v.clock, cx, cy + 14f * u, 40f * u, INK, serif, Paint.Align.CENTER);
|
||||
}
|
||||
|
||||
// ---------- Gift tags: trip, usage, climate, odometer ----------
|
||||
|
||||
private void drawTags(Canvas c, float u) {
|
||||
float l = 28f * u, rr = l + 300f * u, t = 44f * u, tag = 34f * u, step = 38f * u;
|
||||
// The garland they hang from.
|
||||
line.setColor(PINE);
|
||||
line.setStrokeWidth(8f * u);
|
||||
c.drawLine(l + 10f * u, 20f * u, rr - 10f * u, 20f * u, line);
|
||||
fill.setColor(RED);
|
||||
for (int i = 0; i < 7; i++) c.drawCircle(l + 24f * u + i * 44f * u, 20f * u, 4f * u, fill);
|
||||
String[][] rows = {
|
||||
{ "TRIP", fmt(v.tripKm, "%.1f km", "—") },
|
||||
{ "5 MIN", fmt(v.usage5Min, "%.1f kWh/100", "—") },
|
||||
{ "SINCE START", fmt(v.usageTrip, "%.1f kWh/100", "—") },
|
||||
{ "CLIMATE", fmt(v.climateCelsius, "%.0f °C", "—") },
|
||||
{ "ODOMETER", fmt(v.odometerKm, "%,.0f km", "—") },
|
||||
};
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
float top = t + i * step;
|
||||
// A tag with a clipped corner and a hole, hung on a thread from the one above.
|
||||
path.reset();
|
||||
path.moveTo(l + 18f * u, top);
|
||||
path.lineTo(rr, top);
|
||||
path.lineTo(rr, top + tag);
|
||||
path.lineTo(l + 18f * u, top + tag);
|
||||
path.lineTo(l, top + tag / 2f);
|
||||
path.close();
|
||||
fill.setColor(i % 2 == 0 ? 0xFFF6EBD0 : 0xFFEAD9B6);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(RED);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawPath(path, line);
|
||||
line.setColor(0xCCFFFFFF);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawLine(l + 12f * u, top - (i == 0 ? 20f * u - 0f : step - tag), l + 12f * u, top + tag / 2f, line);
|
||||
fill.setColor(0xFF2A1A10);
|
||||
c.drawCircle(l + 12f * u, top + tag / 2f, 3f * u, fill);
|
||||
label(c, rows[i][0], l + 28f * u, top + 23f * u, 17f * u, 0xFF8E1623, thin, Paint.Align.LEFT);
|
||||
label(c, rows[i][1], rr - 12f * u, top + 25f * u, 22f * u, INK, face, Paint.Align.RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The mug of cocoa: the battery ----------
|
||||
|
||||
private void drawMug(Canvas c, float u) {
|
||||
float cx = 178f * u, l = cx - 92f * u, rr = cx + 92f * u, top = 330f * u, bottom = 560f * u;
|
||||
boolean low = v.socPercent != null && v.socPercent < 20f;
|
||||
boolean blinkOn = (SystemClock.uptimeMillis() / 500) % 2 == 0;
|
||||
float corner = 22f * u, inner = 6f * u;
|
||||
// The handle.
|
||||
line.setColor(0xFFCFE6F0);
|
||||
line.setStrokeWidth(14f * u);
|
||||
r.set(rr - 26f * u, top + 50f * u, rr + 40f * u, top + 170f * u);
|
||||
c.drawArc(r, -90f, 180f, false, line);
|
||||
// The glass.
|
||||
r.set(l, top, rr, bottom);
|
||||
fill.setColor(0x30CFE6F0);
|
||||
c.drawRoundRect(r, corner, corner, fill);
|
||||
if (v.socPercent != null) {
|
||||
float share = Math.max(0f, Math.min(1f, v.socPercent / 100f));
|
||||
float level = bottom - inner - share * (bottom - top - 2f * inner);
|
||||
c.save();
|
||||
r.set(l + inner, top + inner, rr - inner, bottom - inner);
|
||||
path.reset();
|
||||
path.addRoundRect(r, corner - inner, corner - inner, Path.Direction.CW);
|
||||
c.clipPath(path);
|
||||
fill.setColor(low ? COLD : COCOA);
|
||||
c.drawRect(l, level, rr, bottom, fill);
|
||||
fill.setColor(0x33FFFFFF);
|
||||
c.drawRect(l, level, rr, level + 4f * u, fill);
|
||||
c.restore();
|
||||
// Marshmallows float on top, bobbing while driving.
|
||||
for (int i = 0; i < 3; i++) {
|
||||
float mx = cx + (i - 1) * 50f * u, my = level - 8f * u + (float) Math.sin(time * 2f + i * 2f) * 2f * u;
|
||||
r.set(mx - 14f * u, my - 12f * u, mx + 14f * u, my + 8f * u);
|
||||
fill.setColor(BONE);
|
||||
c.drawRoundRect(r, 6f * u, 6f * u, fill);
|
||||
}
|
||||
// Steam.
|
||||
line.setColor(0x66FFFFFF);
|
||||
line.setStrokeWidth(4f * u);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
float sx = cx + (i - 1) * 44f * u;
|
||||
path.reset();
|
||||
path.moveTo(sx, level - 24f * u);
|
||||
for (int k = 1; k <= 6; k++) {
|
||||
path.lineTo(sx + (float) Math.sin(time * 2.5f + i + k * 0.9f) * 9f * u, level - 24f * u - k * 11f * u);
|
||||
}
|
||||
c.drawPath(path, line);
|
||||
}
|
||||
}
|
||||
line.setColor(0xFFCFE6F0);
|
||||
line.setStrokeWidth(4f * u);
|
||||
r.set(l, top, rr, bottom);
|
||||
c.drawRoundRect(r, corner, corner, line);
|
||||
String number = fmt(v.socPercent, "%.0f %%", "—");
|
||||
outlined(c, number, cx, 470f * u, 58f * u, low ? 0xFFFF9AA3 : BONE, serif, Paint.Align.CENTER, u);
|
||||
if (low && blinkOn) outlined(c, "BATTERY LOW", cx, 508f * u, 21f * u, BONE, face, Paint.Align.CENTER, u);
|
||||
else if (!low) outlined(c, "BATTERY", cx, 508f * u, 19f * u, GOLD, face, Paint.Align.CENTER, u);
|
||||
}
|
||||
|
||||
// ---------- The signboard and candy canes: the range; baubles: the gears ----------
|
||||
|
||||
private void drawBoard(Canvas c, int w, float u) {
|
||||
float rr = w - 40f * u, l = rr - 250f * u, t = 214f * u, b = 394f * u, mid = (l + rr) / 2f;
|
||||
r.set(l, t, rr, b);
|
||||
fill.setColor(WOOD);
|
||||
c.drawRoundRect(r, 10f * u, 10f * u, fill);
|
||||
line.setColor(0xFF8A5A33);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawRoundRect(r, 10f * u, 10f * u, line);
|
||||
// Snow on top.
|
||||
path.reset();
|
||||
path.moveTo(l - 4f * u, t + 4f * u);
|
||||
path.quadTo(l + 40f * u, t - 24f * u, l + 90f * u, t - 4f * u);
|
||||
path.quadTo(l + 140f * u, t - 28f * u, l + 190f * u, t - 6f * u);
|
||||
path.quadTo(l + 230f * u, t - 16f * u, rr + 4f * u, t + 4f * u);
|
||||
path.lineTo(rr + 4f * u, t + 12f * u);
|
||||
path.lineTo(l - 4f * u, t + 12f * u);
|
||||
path.close();
|
||||
fill.setColor(SNOW);
|
||||
c.drawPath(path, fill);
|
||||
label(c, "RANGE", mid, t + 52f * u, 20f * u, GOLD, face, Paint.Align.CENTER);
|
||||
label(c, fmt(v.rangeKm, "%.0f", "—"), mid, t + 128f * u, 80f * u, BONE, serif, Paint.Align.CENTER);
|
||||
label(c, "KM", mid, t + 160f * u, 20f * u, GOLD, face, Paint.Align.CENTER);
|
||||
// A candy cane per 50 km in the snow below.
|
||||
int lit = Tally.lit(v.rangeKm, 50f, CANES);
|
||||
for (int i = 0; i < CANES; i++) {
|
||||
cane(c, l + 18f * u + i * 30f * u, 488f * u, u, i < lit);
|
||||
}
|
||||
}
|
||||
|
||||
private void cane(Canvas c, float x, float foot, float u, boolean on) {
|
||||
path.reset();
|
||||
path.moveTo(x, foot);
|
||||
path.lineTo(x, foot - 58f * u);
|
||||
path.cubicTo(x, foot - 78f * u, x + 18f * u, foot - 78f * u, x + 18f * u, foot - 62f * u);
|
||||
line.setStrokeWidth(9f * u);
|
||||
line.setColor(on ? 0xFFFFFFFF : 0x40FFFFFF);
|
||||
c.drawPath(path, line);
|
||||
line.setColor(on ? RED : 0x40E53946);
|
||||
line.setPathEffect(new DashPathEffect(new float[] { 7f * u, 7f * u }, 0f));
|
||||
c.drawPath(path, line);
|
||||
line.setPathEffect(null);
|
||||
}
|
||||
|
||||
private void drawGears(Canvas c, int w, float u) {
|
||||
float rr = w - 40f * u, l = rr - 250f * u, cy = 540f * u, rad = 25f * u;
|
||||
String[] gears = { "P", "R", "N", "D" };
|
||||
String now = Gear.label(v.gear);
|
||||
float step = (rr - l) / 4f;
|
||||
for (int i = 0; i < gears.length; i++) {
|
||||
float mid = l + (i + 0.5f) * step;
|
||||
boolean on = gears[i].equals(now);
|
||||
if (on) {
|
||||
fill.setColor(0x55FFD04A);
|
||||
c.drawCircle(mid, cy, 42f * u, fill);
|
||||
}
|
||||
fill.setColor(GOLD);
|
||||
r.set(mid - 7f * u, cy - rad - 9f * u, mid + 7f * u, cy - rad + 3f * u);
|
||||
c.drawRect(r, fill);
|
||||
fill.setColor(on ? GOLD : 0xFF7A1F2A);
|
||||
c.drawCircle(mid, cy, rad, fill);
|
||||
line.setColor(on ? 0xFFFFF0B0 : 0xFFB03A48);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawCircle(mid, cy, rad, line);
|
||||
label(c, gears[i], mid, cy + 11f * u, 30f * u, on ? INK : 0xB0FFFFFF, serif, Paint.Align.CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- What plays: a sleigh or an angel tows it on wrapping paper ----------
|
||||
|
||||
/**
|
||||
* Santa's sleigh and an angel take turns towing a waving strip of wrapping paper with the song
|
||||
* across the sky above the tree, behind the wreath and the tags, again and again while
|
||||
* something plays. The paper's colour is fixed by the song. Returns whether it is drawn, so the
|
||||
* view keeps animating for it.
|
||||
*/
|
||||
private boolean drawSong(Canvas c, int w, float u) {
|
||||
if (v.songTitle == null) return false;
|
||||
text.setTypeface(face);
|
||||
text.setTextSize(19f * u);
|
||||
text.setStyle(Paint.Style.FILL);
|
||||
String s = SongBanner.line(text, v.songTitle, v.songArtist, Math.min(560f * u, w * 0.45f));
|
||||
float t = SystemClock.uptimeMillis() / 1000f;
|
||||
float bw = text.measureText(s) + 40f * u, bh = 28f * u, tow = 80f * u;
|
||||
float bx = SongBanner.left(t, w, bw, tow, u);
|
||||
float yc = 66f * u + (float) Math.sin(t * 1.3f) * 3f * u;
|
||||
int paper = PAPER[Math.floorMod((v.songTitle + "\n" + v.songArtist).hashCode(), PAPER.length)];
|
||||
boolean darkInk = paper == PAPER[3] || paper == PAPER[4];
|
||||
banner.draw(c, fill, line, text, s, bx, yc, bw, bh, t, u, 0f, paper, 0xCCFFFFFF, darkInk ? INK : 0xFFFFFFFF);
|
||||
boolean sleigh = SongBanner.pass(t, w, bw, tow, u) % 2 == 0;
|
||||
float right = bx + bw, cx = right + tow, cy = yc - 6f * u, size = (sleigh ? 70f : 56f) * u;
|
||||
float[] tail = sleigh ? SongCarriers.sleighTail : SongCarriers.angelTail;
|
||||
float ax = cx + tail[0] * size, ay = cy + tail[1] * size;
|
||||
line.setColor(0xCCFFFFFF);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawLine(right, yc - bh / 2f + 3f * u, ax, ay, line);
|
||||
c.drawLine(right, yc + bh / 2f - 3f * u, ax, ay, line);
|
||||
if (sleigh) SongCarriers.sleigh(c, fill, line, path, r, cx, cy, size, t, u);
|
||||
else SongCarriers.angel(c, fill, line, path, r, cx, cy, size, (float) Math.sin(t * 7f), u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ final class EmpireView extends ClusterCanvasView {
|
||||
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path path = new Path();
|
||||
private final SongBanner banner = new SongBanner();
|
||||
private final RectF r = new RectF();
|
||||
private final Typeface serif = Typeface.create(Typeface.SERIF, Typeface.BOLD);
|
||||
private final Typeface sans = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
||||
@@ -61,10 +62,38 @@ final class EmpireView extends ClusterCanvasView {
|
||||
advance(moving);
|
||||
float top = 46f * u, bottom = h - 186f * u;
|
||||
drawWorld(c, w, top, bottom, u);
|
||||
boolean song = drawSong(c, w, top, u);
|
||||
drawSpeed(c, w, top, u);
|
||||
drawResources(c, w, top, u);
|
||||
drawPanel(c, w, h, bottom, u);
|
||||
keepAnimating(moving, 33);
|
||||
keepAnimating(moving || song, 33);
|
||||
}
|
||||
|
||||
// ---------- What plays: an eagle brings it on a scroll ----------
|
||||
|
||||
/**
|
||||
* An eagle flies a waving scroll with the song across the top of the world, behind the speed,
|
||||
* again and again while something plays. Returns whether it is drawn, so the view keeps animating.
|
||||
*/
|
||||
private boolean drawSong(Canvas c, float w, float top, float u) {
|
||||
if (v.songTitle == null) return false;
|
||||
text.setTypeface(serif);
|
||||
text.setTextSize(19f * u);
|
||||
text.setStyle(Paint.Style.FILL);
|
||||
String s = SongBanner.line(text, v.songTitle, v.songArtist, Math.min(560f * u, w * 0.45f));
|
||||
float t = SystemClock.uptimeMillis() / 1000f;
|
||||
float bw = text.measureText(s) + 40f * u, bh = 28f * u, size = 58f * u, tow = size * 1.0f;
|
||||
float bx = SongBanner.left(t, Math.round(w), bw, tow, u);
|
||||
float yc = top + 30f * u + (float) Math.sin(t * 1.3f) * 3f * u;
|
||||
banner.draw(c, fill, line, text, s, bx, yc, bw, bh, t, u, 0f, 0xFFEFE0B8, WOOD_DARK, 0xFF2A1A08);
|
||||
float right = bx + bw, cx = right + tow, cy = yc - 10f * u;
|
||||
line.setColor(0xCCFFFFFF);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
float ax = cx + SongCarriers.eagleTail[0] * size, ay = cy + SongCarriers.eagleTail[1] * size;
|
||||
c.drawLine(right, yc - bh / 2f + 3f * u, ax, ay, line);
|
||||
c.drawLine(right, yc + bh / 2f - 3f * u, ax, ay, line);
|
||||
SongCarriers.eagle(c, fill, line, path, r, cx, cy, size, (float) Math.sin(t * 7f), t, u);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void advance(boolean moving) {
|
||||
|
||||
@@ -217,9 +217,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
/** Whether the Overview hides the navigation (the button in the navigation's header). */
|
||||
static final String PREF_OVERVIEW_FULLSCREEN = "overview_fullscreen";
|
||||
/** The Overview shows what plays (Tiles, Halloween); on by default. */
|
||||
/** The Overview shows what plays (the styles that have a place for it); on by default. */
|
||||
static final String PREF_NOW_PLAYING = "now_playing";
|
||||
/** The Halloween pumpkin's light follows the music; off by default, so the analyser stays off. */
|
||||
/** The Halloween pumpkin's and the Christmas tree's and the Caterpillar's light follows the music; off by default, so the analyser stays off. */
|
||||
static final String PREF_MUSIC_LIGHT = "music_light";
|
||||
|
||||
/** True while the Overview is full screen: the navigation hidden, the edge swipe armed. */
|
||||
@@ -364,10 +364,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
HalloweenView halloweenView = new HalloweenView(this);
|
||||
musicTap = new MusicTap(this);
|
||||
nowPlaying = new NowPlayingWatcher(this);
|
||||
ChristmasView christmasView = new ChristmasView(this);
|
||||
halloweenView.setMusic(musicTap);
|
||||
CaterpillarView caterpillarView = new CaterpillarView(this);
|
||||
christmasView.setMusic(musicTap);
|
||||
caterpillarView.setMusic(musicTap);
|
||||
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
|
||||
new NixieView(this), new ChronographView(this), gridView, gridMapView, new TimeCircuitsView(this),
|
||||
new QuestView(this), visorView, new EmpireView(this), new MuertosView(this), halloweenView);
|
||||
new QuestView(this), visorView, new EmpireView(this), new MuertosView(this), halloweenView,
|
||||
christmasView, caterpillarView);
|
||||
View[] canvasPanes = new View[canvasStyles.size()];
|
||||
for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i));
|
||||
// Grid with map: its map layer goes under it, in the same pane.
|
||||
@@ -1786,7 +1791,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
/** The styles that show what plays. */
|
||||
private static boolean showsNowPlaying(OverviewStyle style) {
|
||||
return style == OverviewStyle.TILES || style == OverviewStyle.HALLOWEEN
|
||||
|| style == OverviewStyle.MUERTOS;
|
||||
|| style == OverviewStyle.MUERTOS || style == OverviewStyle.QUEST || style == OverviewStyle.EMPIRE
|
||||
|| style == OverviewStyle.TERMINAL || style == OverviewStyle.SCIFI
|
||||
|| style == OverviewStyle.CHRISTMAS || style == OverviewStyle.CATERPILLAR;
|
||||
}
|
||||
|
||||
/** The media sessions are watched only while what plays can be seen, and the switch is on. */
|
||||
@@ -1834,13 +1841,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (!android.text.TextUtils.equals(view.getText(), text)) view.setText(text);
|
||||
}
|
||||
|
||||
/** The styles with a light that can follow the music: Halloween's pumpkin, Christmas's tree and the Caterpillar's sun. */
|
||||
private static boolean hasMusicLight(OverviewStyle style) {
|
||||
return style == OverviewStyle.HALLOWEEN || style == OverviewStyle.CHRISTMAS
|
||||
|| style == OverviewStyle.CATERPILLAR;
|
||||
}
|
||||
|
||||
/**
|
||||
* The pumpkin listens only while it can be seen (the app in front, on the Overview, in
|
||||
* Halloween) and only when "Pumpkin light follows the music" is on.
|
||||
* The light listens only while it can be seen (the app in front, on the Overview, in
|
||||
* Halloween, Christmas or Caterpillar) and only when "Light follows the music" is on.
|
||||
*/
|
||||
private void updateMusicTap() {
|
||||
ViewPager2 pager = findViewById(R.id.content);
|
||||
musicTap.setRunning(inFront && overviewStyle() == OverviewStyle.HALLOWEEN
|
||||
musicTap.setRunning(inFront && hasMusicLight(overviewStyle())
|
||||
&& prefs.getBoolean(PREF_MUSIC_LIGHT, false)
|
||||
&& pager != null && pager.getCurrentItem() == 0);
|
||||
}
|
||||
@@ -1908,7 +1921,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
int playing = showsNowPlaying(style) ? View.VISIBLE : View.GONE;
|
||||
settingsPane.findViewById(R.id.now_playing_switch).setVisibility(playing);
|
||||
settingsPane.findViewById(R.id.now_playing_hint).setVisibility(playing);
|
||||
int music = style == OverviewStyle.HALLOWEEN ? View.VISIBLE : View.GONE;
|
||||
int music = hasMusicLight(style) ? View.VISIBLE : View.GONE;
|
||||
settingsPane.findViewById(R.id.music_light_switch).setVisibility(music);
|
||||
settingsPane.findViewById(R.id.music_light_hint).setVisibility(music);
|
||||
}
|
||||
@@ -1926,7 +1939,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
getString(R.string.overview_style_grid_map),
|
||||
getString(R.string.overview_style_time_circuits), getString(R.string.overview_style_quest),
|
||||
getString(R.string.overview_style_visor), getString(R.string.overview_style_empire),
|
||||
getString(R.string.overview_style_muertos), getString(R.string.overview_style_halloween) };
|
||||
getString(R.string.overview_style_muertos), getString(R.string.overview_style_halloween),
|
||||
getString(R.string.overview_style_christmas), getString(R.string.overview_style_caterpillar) };
|
||||
}
|
||||
|
||||
/** The style drop-down; every style is fed on every reading, only the chosen one is shown. */
|
||||
|
||||
@@ -33,7 +33,9 @@ enum OverviewStyle {
|
||||
VISOR(true, true),
|
||||
EMPIRE(false, false),
|
||||
MUERTOS(false, false),
|
||||
HALLOWEEN(false, false);
|
||||
HALLOWEEN(false, false),
|
||||
CHRISTMAS(false, false),
|
||||
CATERPILLAR(false, false);
|
||||
|
||||
static final String PREF_KEY = "overview_style";
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ final class QuestView extends ClusterCanvasView {
|
||||
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint skyPaint = new Paint();
|
||||
private final Path path = new Path();
|
||||
private final SongBanner banner = new SongBanner();
|
||||
private final RectF r = new RectF();
|
||||
private final Typeface serif = Typeface.create(Typeface.SERIF, Typeface.BOLD);
|
||||
private final Typeface sans = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
||||
@@ -54,6 +55,7 @@ final class QuestView extends ClusterCanvasView {
|
||||
if (w <= 0 || h <= 0) return;
|
||||
float u = Math.min(h / 640f, w / 1100f);
|
||||
drawLandscape(c, w, h);
|
||||
boolean song = drawSong(c, w, u);
|
||||
drawPlayer(c, u);
|
||||
drawMinimap(c, w, u);
|
||||
drawSpeed(c, w, u);
|
||||
@@ -62,6 +64,35 @@ final class QuestView extends ClusterCanvasView {
|
||||
drawChat(c, u);
|
||||
drawActionBar(c, w, h, u);
|
||||
drawXp(c, w, h, u);
|
||||
keepAnimating(song, 33);
|
||||
}
|
||||
|
||||
// ---------- What plays: a dragon tows it on a scroll ----------
|
||||
|
||||
/**
|
||||
* A dragon tows a waving scroll with the song across the top of the sky, behind the frames, again
|
||||
* and again while something plays. Returns whether it is drawn, so the view keeps animating.
|
||||
*/
|
||||
private boolean drawSong(Canvas c, float w, float u) {
|
||||
if (v.songTitle == null) return false;
|
||||
text.setTypeface(serif);
|
||||
text.setTextSize(19f * u);
|
||||
text.setStyle(Paint.Style.FILL);
|
||||
String s = SongBanner.line(text, v.songTitle, v.songArtist, Math.min(560f * u, w * 0.45f));
|
||||
float t = SystemClock.uptimeMillis() / 1000f;
|
||||
float bw = text.measureText(s) + 40f * u, bh = 28f * u, size = 58f * u, tow = size * 1.3f;
|
||||
int wi = Math.round(w);
|
||||
float bx = SongBanner.left(t, wi, bw, tow, u);
|
||||
float yc = 72f * u + (float) Math.sin(t * 1.3f) * 3f * u;
|
||||
banner.draw(c, fill, line, text, s, bx, yc, bw, bh, t, u, 0f, 0xFFE8D5A8, GOLD_DARK, 0xFF2A1A08);
|
||||
float right = bx + bw, cx = right + tow, cy = yc - 8f * u;
|
||||
line.setColor(0xCCFFFFFF);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
float ax = cx + SongCarriers.dragonTail[0] * size, ay = cy + SongCarriers.dragonTail[1] * size;
|
||||
c.drawLine(right, yc - bh / 2f + 3f * u, ax, ay, line);
|
||||
c.drawLine(right, yc + bh / 2f - 3f * u, ax, ay, line);
|
||||
SongCarriers.dragon(c, fill, line, path, r, cx, cy, size, (float) Math.sin(t * 6f), t, u);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------- World ----------
|
||||
|
||||
@@ -67,6 +67,13 @@ public final class SciFiView extends View {
|
||||
label(c, redAlert ? "RED ALERT · LOW RESERVES" : "PROPULSION STATUS", w * 0.61f + 10f * d,
|
||||
bar * 0.86f, bar * 1.05f, redAlert ? alert : orange, Paint.Align.LEFT);
|
||||
pill(c, w * 0.94f, 0, w, bar, frame2);
|
||||
if (v.songTitle != null) {
|
||||
// What plays, in black on the top bar, cut to its length.
|
||||
float x = side + 16f * d, size = bar * 0.8f;
|
||||
text.setTextSize(size);
|
||||
String s = SongBanner.line(text, v.songTitle, v.songArtist, w * 0.60f - x - 16f * d);
|
||||
label(c, s, x, bar * 0.78f, size, 0xFF000000, Paint.Align.LEFT);
|
||||
}
|
||||
// The bottom bar's end: odometer and time.
|
||||
String odo = v.odometerKm == null ? "—" : String.format(Locale.US, "%06.0f", v.odometerKm);
|
||||
label(c, "ODO " + odo + " · " + (v.clock.isEmpty() ? "—" : v.clock),
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
|
||||
/**
|
||||
* The creatures that tow a {@link SongBanner} in the styles with a sky for it, drawn flying right
|
||||
* with their centre at (x, y) and about {@code s} wide: a dragon (Quest), an eagle (Empire), Santa's
|
||||
* sleigh with a reindeer and an angel (Christmas), a butterfly and a ladybird (Caterpillar).
|
||||
* {@code flap} runs from −1 to 1 with the wing beat, {@code t} is the time in seconds. The caller
|
||||
* passes its own paints and scratch objects; the paints' colour and width are set here. Where the
|
||||
* tow lines tie on is {@link #dragonTail}, {@link #eagleTail}, {@link #sleighTail}, {@link #angelTail}, {@link #butterflyTail} and {@link #ladybirdTail}, as offsets in units of {@code s}.
|
||||
*/
|
||||
final class SongCarriers {
|
||||
|
||||
static final float[] dragonTail = { -1.0f, -0.25f }, eagleTail = { -0.55f, 0.05f },
|
||||
sleighTail = { -0.62f, 0f }, angelTail = { -0.45f, 0.1f }, butterflyTail = { -0.3f, 0f },
|
||||
ladybirdTail = { -0.4f, 0f };
|
||||
|
||||
private SongCarriers() {}
|
||||
|
||||
static void dragon(Canvas c, Paint fill, Paint line, Path path, RectF r, float x, float y, float s,
|
||||
float flap, float t, float u) {
|
||||
final int red = 0xFFB3262E, dark = 0xFF6E1219, belly = 0xFFE8B84A;
|
||||
c.save();
|
||||
c.translate(x, y);
|
||||
// The wing behind the body: a membrane between three fingers, lifted and dropped by the flap.
|
||||
c.save();
|
||||
c.translate(-0.05f * s, -0.2f * s);
|
||||
c.rotate(-120f + flap * 30f);
|
||||
path.reset();
|
||||
path.moveTo(0, 0);
|
||||
path.lineTo(0.95f * s, -0.05f * s);
|
||||
path.quadTo(0.8f * s, 0.15f * s, 0.62f * s, 0.12f * s);
|
||||
path.quadTo(0.5f * s, 0.3f * s, 0.3f * s, 0.22f * s);
|
||||
path.quadTo(0.2f * s, 0.35f * s, 0, 0.15f * s);
|
||||
path.close();
|
||||
fill.setColor(dark);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(0xFF2A0508);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawPath(path, line);
|
||||
c.drawLine(0, 0.05f * s, 0.62f * s, 0.12f * s, line);
|
||||
c.restore();
|
||||
// The tail, ending in a spade.
|
||||
path.reset();
|
||||
path.moveTo(-0.4f * s, 0f);
|
||||
path.cubicTo(-0.9f * s, 0.1f * s, -0.8f * s, -0.3f * s, -1.0f * s, -0.25f * s);
|
||||
line.setColor(red);
|
||||
line.setStrokeWidth(0.13f * s);
|
||||
line.setStrokeCap(Paint.Cap.ROUND);
|
||||
c.drawPath(path, line);
|
||||
line.setStrokeCap(Paint.Cap.BUTT);
|
||||
// Legs.
|
||||
line.setColor(dark);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawLine(-0.25f * s, 0.12f * s, -0.3f * s, 0.34f * s, line);
|
||||
c.drawLine(0.1f * s, 0.12f * s, 0.18f * s, 0.34f * s, line);
|
||||
// Body with a pale belly.
|
||||
r.set(-0.45f * s, -0.17f * s, 0.3f * s, 0.17f * s);
|
||||
fill.setColor(red);
|
||||
c.drawOval(r, fill);
|
||||
r.set(-0.4f * s, 0.0f, 0.25f * s, 0.15f * s);
|
||||
fill.setColor(belly);
|
||||
c.drawOval(r, fill);
|
||||
// Neck and head, a horn, an eye, and a small flame at the mouth.
|
||||
path.reset();
|
||||
path.moveTo(0.2f * s, -0.05f * s);
|
||||
path.quadTo(0.4f * s, -0.05f * s, 0.5f * s, -0.3f * s);
|
||||
line.setColor(red);
|
||||
line.setStrokeWidth(0.15f * s);
|
||||
c.drawPath(path, line);
|
||||
r.set(0.46f * s, -0.45f * s, 0.86f * s, -0.24f * s);
|
||||
fill.setColor(red);
|
||||
c.drawOval(r, fill);
|
||||
line.setColor(belly);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawLine(0.5f * s, -0.44f * s, 0.38f * s, -0.62f * s, line);
|
||||
fill.setColor(0xFFFFE04A);
|
||||
c.drawCircle(0.62f * s, -0.37f * s, 0.035f * s, fill);
|
||||
fill.setColor(0xFF2A0508);
|
||||
c.drawCircle(0.63f * s, -0.37f * s, 0.016f * s, fill);
|
||||
float flick = 0.8f + 0.2f * (float) Math.sin(t * 25f);
|
||||
fill.setColor(0xFFFF8A1F);
|
||||
c.drawCircle(0.93f * s, -0.33f * s, 0.07f * s * flick, fill);
|
||||
fill.setColor(0xFFFFE04A);
|
||||
c.drawCircle(0.95f * s, -0.33f * s, 0.035f * s * flick, fill);
|
||||
c.restore();
|
||||
}
|
||||
|
||||
static void eagle(Canvas c, Paint fill, Paint line, Path path, RectF r, float x, float y, float s,
|
||||
float flap, float t, float u) {
|
||||
final int brown = 0xFF6B4423, dark = 0xFF3E2610, white = 0xFFF4EAD2, beak = 0xFFF2B21B;
|
||||
c.save();
|
||||
c.translate(x, y);
|
||||
// The tail, a short fan.
|
||||
path.reset();
|
||||
path.moveTo(-0.3f * s, 0f);
|
||||
path.lineTo(-0.58f * s, -0.1f * s);
|
||||
path.lineTo(-0.6f * s, 0.02f * s);
|
||||
path.lineTo(-0.55f * s, 0.14f * s);
|
||||
path.close();
|
||||
fill.setColor(dark);
|
||||
c.drawPath(path, fill);
|
||||
// The far wing, then the near one: feathers fanned from the shoulder, lifted by the flap.
|
||||
for (int wing = 0; wing < 2; wing++) {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
c.save();
|
||||
c.translate(0.02f * s, -0.08f * s);
|
||||
c.rotate(-130f + flap * 32f + i * 13f + (wing == 0 ? 8f : 0f));
|
||||
r.set(0, -0.05f * s, (0.95f - 0.08f * Math.abs(i - 2)) * s, 0.05f * s);
|
||||
fill.setColor(wing == 0 ? dark : (i % 2 == 0 ? brown : 0xFF84552E));
|
||||
c.drawOval(r, fill);
|
||||
c.restore();
|
||||
}
|
||||
}
|
||||
// Body, white head, hooked beak, eye, talons.
|
||||
r.set(-0.35f * s, -0.14f * s, 0.3f * s, 0.14f * s);
|
||||
fill.setColor(brown);
|
||||
c.drawOval(r, fill);
|
||||
fill.setColor(white);
|
||||
c.drawCircle(0.34f * s, -0.12f * s, 0.12f * s, fill);
|
||||
path.reset();
|
||||
path.moveTo(0.42f * s, -0.16f * s);
|
||||
path.quadTo(0.6f * s, -0.15f * s, 0.55f * s, -0.04f * s);
|
||||
path.lineTo(0.43f * s, -0.08f * s);
|
||||
path.close();
|
||||
fill.setColor(beak);
|
||||
c.drawPath(path, fill);
|
||||
fill.setColor(0xFF1A1008);
|
||||
c.drawCircle(0.38f * s, -0.15f * s, 0.022f * s, fill);
|
||||
line.setColor(beak);
|
||||
line.setStrokeWidth(2.5f * u);
|
||||
c.drawLine(-0.1f * s, 0.13f * s, -0.14f * s, 0.26f * s, line);
|
||||
c.drawLine(0.05f * s, 0.13f * s, 0.02f * s, 0.26f * s, line);
|
||||
c.restore();
|
||||
}
|
||||
|
||||
/** A reindeer with a glowing nose pulling Santa's sleigh, drawn about 1.7 s wide. */
|
||||
static void sleigh(Canvas c, Paint fill, Paint line, Path path, RectF r, float x, float y, float s,
|
||||
float t, float u) {
|
||||
final int brown = 0xFF8A5A33, red = 0xFFC62828, gold = 0xFFFFD04A;
|
||||
c.save();
|
||||
c.translate(x, y);
|
||||
// The reindeer: four legs trotting, a body, a neck, a head with antlers and a red nose.
|
||||
float swing = (float) Math.sin(t * 8f) * 0.18f;
|
||||
line.setColor(brown);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawLine(0.72f * s, 0.05f * s, (0.72f + swing) * s, 0.3f * s, line);
|
||||
c.drawLine(0.66f * s, 0.05f * s, (0.66f - swing) * s, 0.3f * s, line);
|
||||
c.drawLine(0.44f * s, 0.05f * s, (0.44f - swing) * s, 0.3f * s, line);
|
||||
c.drawLine(0.38f * s, 0.05f * s, (0.38f + swing) * s, 0.3f * s, line);
|
||||
r.set(0.32f * s, -0.15f * s, 0.8f * s, 0.1f * s);
|
||||
fill.setColor(brown);
|
||||
c.drawOval(r, fill);
|
||||
line.setStrokeWidth(0.1f * s);
|
||||
c.drawLine(0.76f * s, -0.08f * s, 0.86f * s, -0.2f * s, line);
|
||||
c.drawCircle(0.88f * s, -0.22f * s, 0.075f * s, fill);
|
||||
line.setColor(0xFFD9C08A);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawLine(0.85f * s, -0.28f * s, 0.78f * s, -0.46f * s, line);
|
||||
c.drawLine(0.82f * s, -0.37f * s, 0.9f * s, -0.4f * s, line);
|
||||
c.drawLine(0.9f * s, -0.28f * s, 0.97f * s, -0.46f * s, line);
|
||||
c.drawLine(0.94f * s, -0.37f * s, 0.86f * s, -0.42f * s, line);
|
||||
float glow = 0.75f + 0.25f * (float) Math.sin(t * 6f);
|
||||
fill.setColor(0x55FF3B30);
|
||||
c.drawCircle(0.97f * s, -0.2f * s, 0.1f * s * glow, fill);
|
||||
fill.setColor(0xFFFF3B30);
|
||||
c.drawCircle(0.96f * s, -0.2f * s, 0.04f * s, fill);
|
||||
// The harness, and the sleigh: a red body with a curled front on a gold runner.
|
||||
line.setColor(gold);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawLine(0.36f * s, -0.02f * s, 0.12f * s, -0.02f * s, line);
|
||||
path.reset();
|
||||
path.moveTo(-0.55f * s, -0.12f * s);
|
||||
path.lineTo(0.05f * s, -0.12f * s);
|
||||
path.quadTo(0.22f * s, -0.12f * s, 0.2f * s, -0.28f * s);
|
||||
path.lineTo(0.12f * s, 0.1f * s);
|
||||
path.lineTo(-0.5f * s, 0.1f * s);
|
||||
path.quadTo(-0.64f * s, 0.08f * s, -0.6f * s, -0.12f * s);
|
||||
path.close();
|
||||
fill.setColor(red);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(gold);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawPath(path, line);
|
||||
path.reset();
|
||||
path.moveTo(-0.62f * s, 0.19f * s);
|
||||
path.lineTo(0.18f * s, 0.19f * s);
|
||||
path.quadTo(0.32f * s, 0.19f * s, 0.3f * s, 0.07f * s);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawPath(path, line);
|
||||
c.drawLine(-0.4f * s, 0.1f * s, -0.4f * s, 0.19f * s, line);
|
||||
c.drawLine(0.0f, 0.1f * s, 0.0f, 0.19f * s, line);
|
||||
// Santa with his sack.
|
||||
fill.setColor(0xFF7A4A22);
|
||||
c.drawCircle(-0.5f * s, -0.18f * s, 0.11f * s, fill);
|
||||
fill.setColor(red);
|
||||
c.drawCircle(-0.25f * s, -0.2f * s, 0.12f * s, fill);
|
||||
fill.setColor(0xFFF2CFA0);
|
||||
c.drawCircle(-0.22f * s, -0.36f * s, 0.07f * s, fill);
|
||||
fill.setColor(0xFFFFFFFF);
|
||||
r.set(-0.28f * s, -0.37f * s, -0.15f * s, -0.28f * s);
|
||||
c.drawOval(r, fill);
|
||||
path.reset();
|
||||
path.moveTo(-0.29f * s, -0.4f * s);
|
||||
path.lineTo(-0.14f * s, -0.4f * s);
|
||||
path.lineTo(-0.3f * s, -0.56f * s);
|
||||
path.close();
|
||||
fill.setColor(red);
|
||||
c.drawPath(path, fill);
|
||||
fill.setColor(0xFFFFFFFF);
|
||||
c.drawCircle(-0.3f * s, -0.57f * s, 0.035f * s, fill);
|
||||
c.restore();
|
||||
}
|
||||
|
||||
/** A small angel flying right with a trumpet, wings beating. */
|
||||
static void angel(Canvas c, Paint fill, Paint line, Path path, RectF r, float x, float y, float s,
|
||||
float flap, float u) {
|
||||
final int white = 0xFFF4F8FF, gold = 0xFFFFD04A;
|
||||
c.save();
|
||||
c.translate(x, y);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
c.save();
|
||||
c.translate(-0.05f * s, -0.12f * s);
|
||||
c.rotate(-125f + flap * 28f + i * 14f);
|
||||
r.set(0, -0.06f * s, (0.85f - 0.07f * i) * s, 0.06f * s);
|
||||
fill.setColor(white);
|
||||
c.drawOval(r, fill);
|
||||
line.setColor(0xFFB9C6DC);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawOval(r, line);
|
||||
c.restore();
|
||||
}
|
||||
// The robe trails behind, with a gold sash.
|
||||
path.reset();
|
||||
path.moveTo(0.2f * s, -0.14f * s);
|
||||
path.quadTo(0.28f * s, 0.05f * s, 0.05f * s, 0.12f * s);
|
||||
path.lineTo(-0.5f * s, 0.22f * s);
|
||||
path.quadTo(-0.35f * s, -0.02f * s, -0.1f * s, -0.14f * s);
|
||||
path.close();
|
||||
fill.setColor(white);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(0xFFB9C6DC);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawPath(path, line);
|
||||
line.setColor(gold);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawLine(0.02f * s, -0.12f * s, 0.12f * s, 0.06f * s, line);
|
||||
// The head with a halo, and the trumpet held out in front.
|
||||
fill.setColor(0xFFF2CFA0);
|
||||
c.drawCircle(0.3f * s, -0.2f * s, 0.1f * s, fill);
|
||||
fill.setColor(gold);
|
||||
r.set(0.2f * s, -0.4f * s, 0.4f * s, -0.33f * s);
|
||||
c.drawOval(r, line);
|
||||
line.setStrokeWidth(2.5f * u);
|
||||
c.drawLine(0.3f * s, -0.04f * s, 0.7f * s, -0.1f * s, line);
|
||||
path.reset();
|
||||
path.moveTo(0.66f * s, -0.1f * s);
|
||||
path.lineTo(0.9f * s, -0.22f * s);
|
||||
path.lineTo(0.9f * s, 0.02f * s);
|
||||
path.close();
|
||||
c.drawPath(path, fill);
|
||||
c.restore();
|
||||
}
|
||||
|
||||
/** A big tissue-paper butterfly seen from above, flying right, wings beating. */
|
||||
static void butterfly(Canvas c, Paint fill, Paint line, Path path, RectF r, float x, float y, float s,
|
||||
float flap, float u) {
|
||||
final int purple = 0xFF7E57E0, pink = 0xFFFF5FA2, yellow = 0xFFFFE066, dark = 0xFF1B1530;
|
||||
float k = 0.3f + 0.7f * Math.abs(flap);
|
||||
c.save();
|
||||
c.translate(x, y);
|
||||
for (int side = 0; side < 2; side++) {
|
||||
c.save();
|
||||
if (side == 1) c.scale(1f, -1f);
|
||||
path.reset();
|
||||
path.moveTo(0.25f * s, 0);
|
||||
path.quadTo(0.3f * s, -0.8f * s * k, -0.1f * s, -0.95f * s * k);
|
||||
path.quadTo(-0.4f * s, -0.6f * s * k, -0.05f * s, 0);
|
||||
path.close();
|
||||
fill.setColor(purple);
|
||||
c.drawPath(path, fill);
|
||||
path.reset();
|
||||
path.moveTo(-0.05f * s, 0);
|
||||
path.quadTo(-0.45f * s, -0.6f * s * k, -0.7f * s, -0.35f * s * k);
|
||||
path.quadTo(-0.6f * s, -0.05f * s * k, -0.2f * s, 0);
|
||||
path.close();
|
||||
fill.setColor(pink);
|
||||
c.drawPath(path, fill);
|
||||
fill.setColor(yellow);
|
||||
c.drawCircle(0.02f * s, -0.58f * s * k, 0.08f * s, fill);
|
||||
c.drawCircle(-0.4f * s, -0.33f * s * k, 0.06f * s, fill);
|
||||
c.restore();
|
||||
}
|
||||
fill.setColor(dark);
|
||||
r.set(-0.4f * s, -0.06f * s, 0.3f * s, 0.06f * s);
|
||||
c.drawOval(r, fill);
|
||||
c.drawCircle(0.34f * s, 0, 0.07f * s, fill);
|
||||
line.setColor(dark);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawLine(0.38f * s, -0.03f * s, 0.55f * s, -0.18f * s, line);
|
||||
c.drawLine(0.38f * s, 0.03f * s, 0.55f * s, 0.18f * s, line);
|
||||
c.restore();
|
||||
}
|
||||
|
||||
/** A ladybird seen from above, flying right, with its wings out. */
|
||||
static void ladybird(Canvas c, Paint fill, Paint line, Path path, RectF r, float x, float y, float s,
|
||||
float flap, float u) {
|
||||
final int red = 0xFFE53935, dark = 0xFF1B1530;
|
||||
c.save();
|
||||
c.translate(x, y);
|
||||
float out = (0.3f + 0.25f * Math.abs(flap)) * s;
|
||||
fill.setColor(0xAAFFFFFF);
|
||||
r.set(-0.45f * s, -out - 0.12f * s, 0.15f * s, -out + 0.12f * s);
|
||||
c.drawOval(r, fill);
|
||||
r.set(-0.45f * s, out - 0.12f * s, 0.15f * s, out + 0.12f * s);
|
||||
c.drawOval(r, fill);
|
||||
fill.setColor(red);
|
||||
r.set(-0.4f * s, -0.26f * s, 0.26f * s, 0.26f * s);
|
||||
c.drawOval(r, fill);
|
||||
line.setColor(dark);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawLine(-0.4f * s, 0, 0.26f * s, 0, line);
|
||||
fill.setColor(dark);
|
||||
c.drawCircle(-0.2f * s, -0.12f * s, 0.045f * s, fill);
|
||||
c.drawCircle(-0.2f * s, 0.12f * s, 0.045f * s, fill);
|
||||
c.drawCircle(0.05f * s, -0.14f * s, 0.045f * s, fill);
|
||||
c.drawCircle(0.05f * s, 0.14f * s, 0.045f * s, fill);
|
||||
c.drawCircle(0.34f * s, 0, 0.12f * s, fill);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawLine(0.42f * s, -0.06f * s, 0.58f * s, -0.18f * s, line);
|
||||
c.drawLine(0.42f * s, 0.06f * s, 0.58f * s, 0.18f * s, line);
|
||||
c.restore();
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ final class StylePalette {
|
||||
case EMPIRE: return R.style.ThemeOverlay_EV_Style_Empire;
|
||||
case MUERTOS: return R.style.ThemeOverlay_EV_Style_Muertos;
|
||||
case HALLOWEEN: return R.style.ThemeOverlay_EV_Style_Halloween;
|
||||
case CHRISTMAS: return R.style.ThemeOverlay_EV_Style_Christmas;
|
||||
case CATERPILLAR: return R.style.ThemeOverlay_EV_Style_Caterpillar;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,14 @@ final class TerminalView extends ClusterCanvasView {
|
||||
y += line;
|
||||
print(c, row("TYRES", "FL " + v.tyreFl + " FR " + v.tyreFr + " RL " + v.tyreRl + " RR " + v.tyreRr), x, y, green);
|
||||
y += line * 1.4f;
|
||||
if (v.songTitle != null) {
|
||||
// What plays, cut to the row's width.
|
||||
String label = String.format(Locale.US, "%-9s", "PLAYING");
|
||||
String s = v.songTitle + (v.songArtist == null ? "" : " — " + v.songArtist);
|
||||
int fits = text.breakText(s, true, Math.max(0f, getWidth() - x - text.measureText(label)), null);
|
||||
print(c, label + (fits < s.length() ? s.substring(0, Math.max(0, fits - 1)).trim() + "…" : s), x, y, green);
|
||||
y += line;
|
||||
}
|
||||
boolean cursor = (SystemClock.uptimeMillis() / 500) % 2 == 0;
|
||||
print(c, "[" + (v.clock.isEmpty() ? "--:--" : v.clock) + "] nils@mg4:~$ " + (cursor ? "█" : ""), x, y, green);
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<TextView android:id="@+id/accent_note" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
||||
<!-- Only the styles that show what plays (Tiles, Halloween). -->
|
||||
<!-- Only the styles that show what plays (the styles with a place for it). -->
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/now_playing_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_now_playing" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" android:visibility="gone" />
|
||||
<TextView android:id="@+id/now_playing_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_now_playing" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
||||
<!-- Only Halloween listens to the music. -->
|
||||
<!-- Only Halloween, Christmas and Caterpillar listen to the music. -->
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/music_light_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_music_light" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" android:visibility="gone" />
|
||||
<TextView android:id="@+id/music_light_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_music_light" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:baselineAligned="false">
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/accent_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<TextView android:id="@+id/accent_note" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
||||
<!-- Only the styles that show what plays (Tiles, Halloween). -->
|
||||
<!-- Only the styles that show what plays (the styles with a place for it). -->
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/now_playing_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_now_playing" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" android:visibility="gone" />
|
||||
<TextView android:id="@+id/now_playing_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_now_playing" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
||||
<!-- Only Halloween listens to the music. -->
|
||||
<!-- Only Halloween, Christmas and Caterpillar listen to the music. -->
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/music_light_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_music_light" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" android:visibility="gone" />
|
||||
<TextView android:id="@+id/music_light_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_music_light" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_tire_unit" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="20dp" />
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
<string name="now_playing_radio">Radio</string>
|
||||
<string name="now_playing_other">Media</string>
|
||||
<string name="now_playing_paused">%1$s · paused</string>
|
||||
<string name="label_music_light">Pumpkin light follows the music</string>
|
||||
<string name="label_music_light">Light follows the music</string>
|
||||
<string name="hint_music_light">Flares on the kick drum over Bluetooth or CarPlay, not the radio. Levels only, no sound kept.</string>
|
||||
<string name="accent_not_used">%1$s has its own colours, and the app takes them on. To change the accent colour, pick a style that uses it.</string>
|
||||
<string name="overview_fullscreen_enter">Overview full screen: hide the navigation</string>
|
||||
@@ -152,6 +152,8 @@
|
||||
<string name="overview_style_empire">Empire</string>
|
||||
<string name="overview_style_muertos">Día de Muertos</string>
|
||||
<string name="overview_style_halloween">Halloween</string>
|
||||
<string name="overview_style_christmas">Christmas</string>
|
||||
<string name="overview_style_caterpillar">Caterpillar</string>
|
||||
<string name="overview_style_nixie">Nixie tubes</string>
|
||||
<string name="overview_style_chronograph">Chronograph</string>
|
||||
<string name="cockpit_battery">BATTERY</string>
|
||||
|
||||
@@ -124,4 +124,24 @@
|
||||
<item name="evAccentStroke">#80804A1E</item>
|
||||
<item name="evAccentTrack">#6A3F18</item>
|
||||
</style>
|
||||
<!-- Christmas: the red of the big bauble. -->
|
||||
<style name="ThemeOverlay.EV.Style.Christmas" parent="">
|
||||
<item name="evAccent">#E53946</item>
|
||||
<item name="evOnAccent">#1F0306</item>
|
||||
<item name="evAccentLight">#FF9AA3</item>
|
||||
<item name="evAccentDeep">#3D0C12</item>
|
||||
<item name="evAccentSurface">#10243A</item>
|
||||
<item name="evAccentStroke">#80B08A3A</item>
|
||||
<item name="evAccentTrack">#7A2630</item>
|
||||
</style>
|
||||
<!-- Caterpillar: the green of its body. -->
|
||||
<style name="ThemeOverlay.EV.Style.Caterpillar" parent="">
|
||||
<item name="evAccent">#5CC15A</item>
|
||||
<item name="evOnAccent">#07210B</item>
|
||||
<item name="evAccentLight">#B6F0A8</item>
|
||||
<item name="evAccentDeep">#123A1A</item>
|
||||
<item name="evAccentSurface">#10243A</item>
|
||||
<item name="evAccentStroke">#8057A04A</item>
|
||||
<item name="evAccentTrack">#2E6B35</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 313 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 282 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 317 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
Reference in New Issue
Block a user