Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,23 @@ A Watch Face Format (WFF) v1 watch face featuring a fire-service aerial ladder t
- Fire engine seen from above = hour hand
- Turntable = center
- Extended aerial ladder + rescue basket = minute hand
- The two blue emergency lights flash alternately; when they do is configurable in the watch face editor on the watch (long-press the watch face → edit): "Off", "On the hour" (default, active during minute 00) or "Always on"
- The blue lights are disabled in ambient / always-on display mode
- The dial fades between a night and a daylight background, crossfading over an hour at dawn and dusk. At night the head lamps, their beams and the tail lamps are lit.
- The water tank on the truck doubles as the battery gauge: the fill level is the charge level, and below 15 % the remaining water and the empty volume turn red.
- A call-sign plate on the roof carries the date as `DD/MM`. It rides on the truck, so it turns with the hour hand.
- The two blue emergency lights flash alternately; when they do is configurable: "Off", "On the hour" (default, active during minute 00), "On notifications" (while unread notifications are pending) or "Always on"
- The blue lights, the driving lights and the day background are disabled in ambient / always-on display mode
- The subtle dial reacts slightly to the watch's tilt; the actual hands stay geometrically fixed to the center so the time remains accurate

Water tank and call-sign plate can each be switched off in the watch face editor on the watch (long-press the watch face → edit), along with the blue-light mode.

## Screenshots

Mock-ups rendered from the watch face geometry, not photographs of a device.

![Day, dusk and night](docs/screenshots/day-night.png)

![Water tank, notification mode and plain dial](docs/screenshots/features.png)

## Project

The watch face is implemented declaratively in `watchface/src/main/res/raw/watchface.xml`. It intentionally contains no app code; the manifest uses `android:hasCode="false"`.
Expand Down Expand Up @@ -61,5 +74,6 @@ Updates install the same way as a first install; since all releases are signed w
## Notes

- The project targets Wear OS API 33+, which makes it suitable for the Pixel Watch 1.
- The blue lights flash intentionally instead of staying lit. The logic lives directly in `watchface.xml`; the off/hourly/always behavior is a WFF `ListConfiguration` user setting. Note that "Always on" costs noticeably more battery while the display is active.
- The blue lights flash intentionally instead of staying lit. The logic lives directly in `watchface.xml`; the behavior is a WFF `ListConfiguration` user setting. Note that "Always on" costs noticeably more battery while the display is active.
- Day and night are derived from the local time (`SECONDS_IN_DAY`), not from sunrise data, so no weather provider or extra permission is involved.
- The geometry is built entirely from WFF vector primitives; the watch face itself has no bitmap dependency.
Binary file added docs/screenshots/day-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/features.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
210 changes: 208 additions & 2 deletions watchface/src/main/res/raw/watchface.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,46 @@
defaultValue="hourly">
<ListOption id="off" displayName="@string/blue_light_mode_off" />
<ListOption id="hourly" displayName="@string/blue_light_mode_hourly" />
<ListOption id="notification" displayName="@string/blue_light_mode_notification" />
<ListOption id="always" displayName="@string/blue_light_mode_always" />
</ListConfiguration>

<BooleanConfiguration id="showWaterTank"
displayName="@string/water_tank"
screenReaderText="@string/water_tank"
defaultValue="TRUE" />

<BooleanConfiguration id="showCallSign"
displayName="@string/call_sign"
screenReaderText="@string/call_sign"
defaultValue="TRUE" />
</UserConfigurations>

<Scene backgroundColor="#000000">
<!--
Daylight background.
The scene itself is black (night). This layer fades in over it, so the
crossfade between night and day is a single alpha ramp:
dawn 06:00 - 07:00
dusk 19:00 - 20:00
Both clamps saturate at 1 during the day, so their product is the
daylight factor. It is reused (inverted) by the night lights below.
-->
<Group x="0" y="0" width="450" height="450" name="day_background">
<Variant mode="AMBIENT" target="alpha" value="0" />
<Transform target="alpha"
value="255 * clamp(([SECONDS_IN_DAY] - 21600) / 3600, 0, 1)
* clamp((72000 - [SECONDS_IN_DAY]) / 3600, 0, 1)" />
<PartDraw x="0" y="0" width="450" height="450">
<Ellipse x="0" y="0" width="450" height="450">
<Fill color="#101C28" />
</Ellipse>
<Ellipse x="43" y="43" width="364" height="364">
<Fill color="#16242F" />
</Ellipse>
</PartDraw>
</Group>

<!-- Subtle orientation-reactive dial. The time-bearing parts remain fixed to the center. -->
<Group x="0" y="0" width="450" height="450" name="tilt_dial" alpha="150">
<Variant mode="AMBIENT" target="alpha" value="80" />
Expand Down Expand Up @@ -74,6 +109,138 @@
<Rectangle x="196" y="183" width="58" height="38"><Fill color="#A9161E" /></Rectangle>
<Rectangle x="196" y="231" width="58" height="44"><Fill color="#A9161E" /></Rectangle>
<Rectangle x="197" y="286" width="56" height="13"><Fill color="#F5F5F5" /></Rectangle>
</PartDraw>

<!--
Water tank as a battery gauge.
The water rectangle spans the full tank window and is scaled
vertically around its lower edge (y = 282 of 450), so the fill
level equals the charge level.
-->
<Condition>
<Expressions>
<Expression name="show_water_tank">
<![CDATA[[CONFIGURATION.showWaterTank] == "TRUE"]]>
</Expression>
</Expressions>

<Compare expression="show_water_tank">
<Group x="0" y="0" width="450" height="450" name="water_tank">
<!-- Tank well -->
<PartDraw x="0" y="0" width="450" height="450">
<Rectangle x="198" y="248" width="54" height="34">
<Fill color="#161D24" />
</Rectangle>
</PartDraw>

<!--
Reserve warning. An almost empty tank leaves too little
red water to notice, so the empty volume carries the
signal as well.
-->
<Group x="0" y="0" width="450" height="450" name="tank_reserve_well">
<Transform target="alpha" value="[BATTERY_PERCENT] &lt;= 15 ? 255 : 0" />
<PartDraw x="0" y="0" width="450" height="450">
<Rectangle x="198" y="248" width="54" height="34">
<Fill color="#40100C" />
</Rectangle>
</PartDraw>
</Group>

<!-- Water level, scaled around the lower tank edge (y = 282 of 450) -->
<Group x="0" y="0" width="450" height="450" name="water_level"
pivotX="0.5" pivotY="0.6267">
<Transform target="scaleY" value="clamp([BATTERY_PERCENT] / 100, 0, 1)" />
<PartDraw x="0" y="0" width="450" height="450">
<Rectangle x="198" y="248" width="54" height="34">
<Fill color="#2E7FE0" />
</Rectangle>
<Rectangle x="198" y="248" width="54" height="3">
<Fill color="#8FC4FF" />
</Rectangle>
</PartDraw>

<!-- Reserve: the remaining water turns red below 15 %. -->
<Group x="0" y="0" width="450" height="450" name="water_reserve">
<Transform target="alpha" value="[BATTERY_PERCENT] &lt;= 15 ? 255 : 0" />
<PartDraw x="0" y="0" width="450" height="450">
<Rectangle x="198" y="248" width="54" height="34">
<Fill color="#D8412F" />
</Rectangle>
<Rectangle x="198" y="248" width="54" height="3">
<Fill color="#FF9C8A" />
</Rectangle>
</PartDraw>
</Group>
</Group>

<!-- Sight glass frame -->
<PartDraw x="0" y="0" width="450" height="450">
<Rectangle x="198" y="248" width="54" height="34">
<Stroke color="#F0F1F2" thickness="2" />
</Rectangle>
<Rectangle x="198" y="264" width="8" height="2">
<Fill color="#F0F1F2" />
</Rectangle>
<Rectangle x="244" y="264" width="8" height="2">
<Fill color="#F0F1F2" />
</Rectangle>
</PartDraw>

<!--
Reserve warning: an almost empty tank leaves too little
red water to notice, so the frame carries the signal.
-->
<Group x="0" y="0" width="450" height="450" name="tank_reserve_frame">
<Transform target="alpha" value="[BATTERY_PERCENT] &lt;= 15 ? 255 : 0" />
<PartDraw x="0" y="0" width="450" height="450">
<Rectangle x="198" y="248" width="54" height="34">
<Stroke color="#FF6A57" thickness="3" />
</Rectangle>
</PartDraw>
</Group>
</Group>
</Compare>
</Condition>

<!--
Call-sign plate on the roof, carrying the date.
It rides on the truck and therefore turns with the hour hand,
so it is upside down or sideways for part of the day - which is
exactly how a plate on a real vehicle behaves.
-->
<Condition>
<Expressions>
<Expression name="show_call_sign">
<![CDATA[[CONFIGURATION.showCallSign] == "TRUE"]]>
</Expression>
</Expressions>

<Compare expression="show_call_sign">
<Group x="0" y="0" width="450" height="450" name="call_sign">
<PartDraw x="0" y="0" width="450" height="450">
<RoundRectangle x="197" y="128" width="56" height="26"
cornerRadiusX="4" cornerRadiusY="4">
<Fill color="#EFF1F3" />
</RoundRectangle>
<RoundRectangle x="197" y="128" width="56" height="26"
cornerRadiusX="4" cornerRadiusY="4">
<Stroke color="#15171A" thickness="2" />
</RoundRectangle>
</PartDraw>

<PartText x="197" y="131" width="56" height="20">
<Text align="CENTER">
<Font family="sans-serif" size="19" weight="BOLD" color="#15171A">
<Template>%s/%s<Parameter expression="[DAY_Z]" /><Parameter expression="[MONTH_Z]" /></Template>
</Font>
</Text>
</PartText>
</Group>
</Compare>
</Condition>

<PartDraw x="0" y="0" width="450" height="450">

<!-- Wheels -->
<RoundRectangle x="178" y="118" width="15" height="44" cornerRadiusX="5" cornerRadiusY="5"><Fill color="#141518" /></RoundRectangle>
Expand All @@ -86,15 +253,54 @@
<Ellipse x="247" y="66" width="12" height="8"><Fill color="#E9D98A" /></Ellipse>
</PartDraw>

<!--
Driving lights, active at night only. The alpha is the inverse of
the daylight factor used by the background, so lights and
background cross-fade together.
-->
<Group x="0" y="0" width="450" height="450" name="driving_lights">
<Variant mode="AMBIENT" target="alpha" value="0" />
<Transform target="alpha"
value="255 * (1 - clamp(([SECONDS_IN_DAY] - 21600) / 3600, 0, 1)
* clamp((72000 - [SECONDS_IN_DAY]) / 3600, 0, 1))" />

<!-- Head lamps lit -->
<PartDraw x="0" y="0" width="450" height="450">
<Ellipse x="189" y="64" width="16" height="12"><Fill color="#FFF3C4" /></Ellipse>
<Ellipse x="245" y="64" width="16" height="12"><Fill color="#FFF3C4" /></Ellipse>
</PartDraw>

<!-- Beams cast ahead of the truck -->
<PartDraw x="0" y="0" width="450" height="450" alpha="55">
<Ellipse x="177" y="22" width="42" height="52"><Fill color="#FFF0B8" /></Ellipse>
<Ellipse x="231" y="22" width="42" height="52"><Fill color="#FFF0B8" /></Ellipse>
</PartDraw>

<!-- Tail lamps -->
<PartDraw x="0" y="0" width="450" height="450">
<RoundRectangle x="191" y="293" width="16" height="10" cornerRadiusX="3" cornerRadiusY="3">
<Fill color="#E23B2E" />
</RoundRectangle>
<RoundRectangle x="243" y="293" width="16" height="10" cornerRadiusX="3" cornerRadiusY="3">
<Fill color="#E23B2E" />
</RoundRectangle>
</PartDraw>
<PartDraw x="0" y="0" width="450" height="450" alpha="90">
<Ellipse x="185" y="287" width="28" height="22"><Fill color="#E23B2E" /></Ellipse>
<Ellipse x="237" y="287" width="28" height="22"><Fill color="#E23B2E" /></Ellipse>
</PartDraw>
</Group>

<!--
Emergency lights.
Driven by the user configuration "blueLightMode":
off / hourly (active during minute 00) / always.
off / hourly (active during minute 00) / notification (while
unread notifications are pending) / always.
-->
<Condition>
<Expressions>
<Expression name="blue_lights_active">
<![CDATA[[CONFIGURATION.blueLightMode] == "always" || ([CONFIGURATION.blueLightMode] == "hourly" && [MINUTE] == 0)]]>
<![CDATA[[CONFIGURATION.blueLightMode] == "always" || ([CONFIGURATION.blueLightMode] == "hourly" && [MINUTE] == 0) || ([CONFIGURATION.blueLightMode] == "notification" && [UNREAD_NOTIFICATION_COUNT] > 0)]]>
</Expression>
</Expressions>

Expand Down
3 changes: 3 additions & 0 deletions watchface/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
<string name="blue_light_mode">Blaulicht</string>
<string name="blue_light_mode_off">Aus</string>
<string name="blue_light_mode_hourly">Zur vollen Stunde</string>
<string name="blue_light_mode_notification">Bei Benachrichtigungen</string>
<string name="blue_light_mode_always">Immer an</string>
<string name="water_tank">Löschwassertank (Akku)</string>
<string name="call_sign">Funkrufname (Datum)</string>
</resources>
3 changes: 3 additions & 0 deletions watchface/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
<string name="blue_light_mode">Blue lights</string>
<string name="blue_light_mode_off">Off</string>
<string name="blue_light_mode_hourly">On the hour</string>
<string name="blue_light_mode_notification">On notifications</string>
<string name="blue_light_mode_always">Always on</string>
<string name="water_tank">Water tank (battery)</string>
<string name="call_sign">Call sign (date)</string>
</resources>
Loading