Compare commits
4 Commits
683569dca5
...
147db823b7
| Author | SHA1 | Date | |
|---|---|---|---|
| 147db823b7 | |||
| 710c4a0cd0 | |||
| 23c0bb95bc | |||
| 7803d440ea |
153
CLAUDE.md
Normal file
153
CLAUDE.md
Normal file
@ -0,0 +1,153 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a novelty Garmin Connect IQ watchface that displays time as decimal day progress (French Revolutionary Time style) instead of traditional 12/24 hour format. The day is divided into 10 equal parts, with time displayed as a decimal from 0.0 to 10.0.
|
||||
|
||||
**Key Concept**: The watchface converts standard time to decimal using the formula:
|
||||
```
|
||||
decimalTime = (hours + minutes/60 + seconds/3600) / 24 * 10
|
||||
```
|
||||
|
||||
This means:
|
||||
- Midnight (00:00) → 0.0
|
||||
- 6:00 AM → 2.5
|
||||
- Noon (12:00) → 5.0
|
||||
- 6:00 PM → 7.5
|
||||
- End of day → 10.0
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Building the Watchface
|
||||
|
||||
The project uses the Garmin Connect IQ SDK. You need the SDK installed with `$SDK_PATH` environment variable set.
|
||||
|
||||
**Build for simulator:**
|
||||
```bash
|
||||
$SDK_PATH/bin/monkeyc \
|
||||
-o bin/MetricWatchFace.prg \
|
||||
-f monkey.jungle \
|
||||
-y $SDK_PATH/bin/developer_key
|
||||
```
|
||||
|
||||
**Run in simulator:**
|
||||
```bash
|
||||
$SDK_PATH/bin/connectiq
|
||||
# Then load the generated .prg file in the simulator
|
||||
```
|
||||
|
||||
**Build for specific device (example for Fenix 7):**
|
||||
```bash
|
||||
$SDK_PATH/bin/monkeyc \
|
||||
-o bin/MetricWatchFace.prg \
|
||||
-f monkey.jungle \
|
||||
-d fenix7 \
|
||||
-y $SDK_PATH/bin/developer_key
|
||||
```
|
||||
|
||||
**Note**: The project uses `monkey.jungle` for build configuration, which references `manifest.xml` for project settings.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
**DecimalWatchFaceApp.mc** (`source/DecimalWatchFaceApp.mc:4-23`)
|
||||
- Application entry point extending `Application.AppBase`
|
||||
- Minimal setup - primarily returns the watchface view
|
||||
- No complex application state management
|
||||
|
||||
**DecimalWatchFaceView.mc** (`source/DecimalWatchFaceView.mc:8-201`)
|
||||
- Main watchface implementation extending `WatchUi.WatchFace`
|
||||
- Handles all rendering and time calculation logic
|
||||
- Key methods:
|
||||
- `onUpdate(dc)` - Main render loop, called every second when active
|
||||
- `computeDecimalTime(clockTime)` - Converts standard time to decimal (0.0-10.0)
|
||||
- `drawWatchFace(dc)` - Renders the analog face with 10 divisions, tick marks, and numbers
|
||||
- `drawHand(dc, decimalTime)` - Draws both hour and minute hands
|
||||
- `drawSingleHand(...)` - Helper for rendering individual hands as filled polygons
|
||||
- `drawDigitalTime(dc, decimalTime)` - Displays decimal time as text
|
||||
|
||||
### Rendering Architecture
|
||||
|
||||
The watchface uses programmatic drawing (no XML layouts). All graphics are drawn using Monkey C Graphics API:
|
||||
|
||||
1. **Watchface Circle**: 100 tick marks drawn in a loop (10 major at whole numbers 0-9, 90 minor at 0.1 intervals)
|
||||
2. **Two Hands**:
|
||||
- **Hour hand** (red, shorter, wider): Shows which decimal unit (0-9), completes one rotation per day
|
||||
- **Minute hand** (white, longer, thinner): Shows fractional part within current decimal unit, completes 10 rotations per day
|
||||
3. **Digital Display**: Shows exact decimal time at bottom (formatted to 2 decimal places)
|
||||
|
||||
The hand rotation uses trigonometry with angles calculated from decimal time, offset by -90° to start at top (12 o'clock position).
|
||||
|
||||
### Device Support
|
||||
|
||||
The manifest (`manifest.xml`) targets 57 modern Garmin devices including:
|
||||
- Fenix series (6, 7, 8, Epix 2)
|
||||
- Forerunner series (255, 265, 645, 945, 955, 965, 970)
|
||||
- Vivoactive series (3, 4, 5, 6)
|
||||
- Venu series (2 Plus, 3, 4)
|
||||
|
||||
All devices require Connect IQ API level 3.2.0 or higher.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
MetricWatchFace/
|
||||
├── manifest.xml # App metadata, device compatibility
|
||||
├── monkey.jungle # Build configuration
|
||||
├── source/
|
||||
│ ├── DecimalWatchFaceApp.mc # Application entry point
|
||||
│ └── DecimalWatchFaceView.mc # Main watchface view and rendering
|
||||
└── resources/
|
||||
├── drawables/
|
||||
│ ├── drawables.xml # Drawable resource definitions
|
||||
│ └── launcher_icon.png # App icon (placeholder)
|
||||
└── strings/
|
||||
└── strings.xml # Localized strings
|
||||
```
|
||||
|
||||
## Important Implementation Details
|
||||
|
||||
### Time Calculation Edge Cases
|
||||
- The decimal time formula ensures smooth progression from 0.0 to 10.0
|
||||
- The fractional part extraction (`decimalTime - decimalTime.toNumber()`) is used for the minute hand
|
||||
- Both hands update every second when the watchface is active
|
||||
|
||||
### Drawing Performance
|
||||
- The watchface redraws completely on each `onUpdate()` call
|
||||
- 100 tick marks are drawn in every frame (major optimization opportunity)
|
||||
- Hands are drawn as filled polygons using triangle geometry
|
||||
- Center dot is redrawn after hands to ensure it's on top
|
||||
|
||||
### Sleep Mode Handling
|
||||
- `onEnterSleep()` and `onExitSleep()` are currently empty
|
||||
- In sleep mode, the system reduces update frequency automatically
|
||||
- Consider implementing reduced complexity rendering for sleep mode
|
||||
|
||||
## Known Technical Debt
|
||||
|
||||
From IMPROVEMENT_IDEAS.md:
|
||||
- Launcher icon is currently a placeholder PNG
|
||||
- No screen type optimization (MIP vs AMOLED)
|
||||
- No low power mode implementation
|
||||
- Hand polygon drawing could be optimized
|
||||
- No error handling for edge cases
|
||||
- No unit tests for time conversion
|
||||
|
||||
## Monkey C Specifics
|
||||
|
||||
**Language**: Monkey C (Garmin's proprietary language, similar to Java/C)
|
||||
**Key APIs used**:
|
||||
- `Toybox.WatchUi.WatchFace` - Base class for watchfaces
|
||||
- `Toybox.Graphics` - Drawing primitives (drawLine, fillPolygon, drawText, etc.)
|
||||
- `Toybox.System` - System functions (getClockTime)
|
||||
- `Toybox.Time` / `Toybox.Time.Gregorian` - Time utilities
|
||||
- `Toybox.Lang` / `Toybox.Math` - Language and math utilities
|
||||
|
||||
**Common patterns**:
|
||||
- Drawing context (`dc`) is passed to all render methods
|
||||
- Colors use `Graphics.COLOR_*` constants
|
||||
- Coordinates are in pixels, origin at top-left
|
||||
- All trigonometry uses radians (convert with `Math.toRadians()`)
|
||||
@ -8,7 +8,7 @@ This document contains potential enhancements and features to improve the decima
|
||||
|
||||
### 1. Better Graphics & Styling
|
||||
- **Proper launcher icon** - Replace placeholder with actual designed icon
|
||||
- **Sub-divisions/minor tick marks** - Add marks at 0.5 intervals for finer precision
|
||||
- **Sub-divisions/minor tick marks** - Add marks at 0.5 intervals for finer precision (/)
|
||||
- **Color themes**
|
||||
- Dark mode / Light mode
|
||||
- "Metric blue/orange" scheme
|
||||
|
||||
5
resources/properties.xml
Normal file
5
resources/properties.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<properties>
|
||||
<property id="UseFiveHourMode" type="boolean">false</property>
|
||||
<property id="Show12HourShadow" type="boolean">false</property>
|
||||
<property id="ShowNumericTime" type="boolean">true</property>
|
||||
</properties>
|
||||
11
resources/settings/settings.xml
Normal file
11
resources/settings/settings.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<settings>
|
||||
<setting propertyKey="@Properties.UseFiveHourMode" title="@Strings.FiveHourModeTitle">
|
||||
<settingConfig type="boolean" />
|
||||
</setting>
|
||||
<setting propertyKey="@Properties.Show12HourShadow" title="@Strings.Show12HourShadowTitle">
|
||||
<settingConfig type="boolean" />
|
||||
</setting>
|
||||
<setting propertyKey="@Properties.ShowNumericTime" title="@Strings.ShowNumericTimeTitle">
|
||||
<settingConfig type="boolean" />
|
||||
</setting>
|
||||
</settings>
|
||||
@ -1,3 +1,6 @@
|
||||
<strings>
|
||||
<string id="AppName">Decimal Day</string>
|
||||
<string id="FiveHourModeTitle">5-Hour Mode</string>
|
||||
<string id="Show12HourShadowTitle">12 Hour Shadow</string>
|
||||
<string id="ShowNumericTimeTitle">Show Numeric Time</string>
|
||||
</strings>
|
||||
|
||||
@ -4,11 +4,34 @@ using Toybox.System;
|
||||
using Toybox.Lang;
|
||||
using Toybox.Time;
|
||||
using Toybox.Time.Gregorian;
|
||||
using Toybox.Application;
|
||||
|
||||
class DecimalWatchFaceView extends WatchUi.WatchFace {
|
||||
|
||||
var isHighPowerMode = true; // Track if we're in high power mode
|
||||
|
||||
// Custom colors for AMOLED-friendly shadows
|
||||
const VERY_DARK_RED = 0x330000; // #330000
|
||||
const VERY_DARK_GRAY = 0x333333; // #333333
|
||||
|
||||
var useAmoledColors = false; // Whether to use darker colors
|
||||
|
||||
function initialize() {
|
||||
WatchFace.initialize();
|
||||
|
||||
// Check if device likely has AMOLED screen
|
||||
// AMOLED devices typically support always-on display and higher color depths
|
||||
var deviceSettings = System.getDeviceSettings();
|
||||
|
||||
// Check if device supports always-on display (common on AMOLED)
|
||||
// or if we can detect high color support
|
||||
if (deviceSettings has :requiresBurnInProtection) {
|
||||
useAmoledColors = deviceSettings.requiresBurnInProtection;
|
||||
} else {
|
||||
// Fallback: use darker colors if device has round screen (most AMOLED watches)
|
||||
// This is a heuristic, but safer to use dark colors on all devices
|
||||
useAmoledColors = true; // Use darker colors by default for burn-in protection
|
||||
}
|
||||
}
|
||||
|
||||
// Load your resources here
|
||||
@ -28,18 +51,44 @@ class DecimalWatchFaceView extends WatchUi.WatchFace {
|
||||
var clockTime = System.getClockTime();
|
||||
var decimalTime = computeDecimalTime(clockTime);
|
||||
|
||||
// Check settings
|
||||
var useFiveHourMode = Application.Properties.getValue("UseFiveHourMode");
|
||||
var show12HourShadow = Application.Properties.getValue("Show12HourShadow");
|
||||
var showNumericTime = Application.Properties.getValue("ShowNumericTime");
|
||||
var displayTime = decimalTime;
|
||||
var isMorning = true;
|
||||
|
||||
if (useFiveHourMode) {
|
||||
// Convert 0-10 to 0-5 twice per day
|
||||
if (decimalTime >= 5.0) {
|
||||
displayTime = decimalTime - 5.0;
|
||||
isMorning = false; // Afternoon
|
||||
} else {
|
||||
displayTime = decimalTime;
|
||||
isMorning = true; // Morning
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the screen
|
||||
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK);
|
||||
dc.clear();
|
||||
|
||||
// Draw the watchface
|
||||
drawWatchFace(dc);
|
||||
// Draw the watchface (circle, ticks, numbers)
|
||||
drawWatchFace(dc, useFiveHourMode, isMorning);
|
||||
|
||||
// Draw the hand
|
||||
drawHand(dc, decimalTime);
|
||||
|
||||
// Draw digital time display
|
||||
// Draw digital time displays (before hands so hands appear on top)
|
||||
if (showNumericTime) {
|
||||
// Draw decimal time at bottom
|
||||
drawDigitalTime(dc, decimalTime);
|
||||
|
||||
// If 12-hour shadow is also enabled, draw traditional time at top
|
||||
if (show12HourShadow) {
|
||||
drawTraditionalTime(dc, clockTime);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the hands last (on top of everything)
|
||||
drawHand(dc, displayTime, useFiveHourMode, show12HourShadow, clockTime);
|
||||
}
|
||||
|
||||
// Called when this View is removed from the screen. Save the
|
||||
@ -50,10 +99,12 @@ class DecimalWatchFaceView extends WatchUi.WatchFace {
|
||||
|
||||
// The user has just looked at their watch. Timers and animations may be started here.
|
||||
function onExitSleep() {
|
||||
isHighPowerMode = true;
|
||||
}
|
||||
|
||||
// Terminate any active timers and prepare for slow updates.
|
||||
function onEnterSleep() {
|
||||
isHighPowerMode = false;
|
||||
}
|
||||
|
||||
// Compute decimal time from current clock time
|
||||
@ -70,7 +121,7 @@ class DecimalWatchFaceView extends WatchUi.WatchFace {
|
||||
}
|
||||
|
||||
// Draw the watchface circle, tick marks, and numbers
|
||||
function drawWatchFace(dc) {
|
||||
function drawWatchFace(dc, useFiveHourMode, isMorning) {
|
||||
var width = dc.getWidth();
|
||||
var height = dc.getHeight();
|
||||
var centerX = width / 2;
|
||||
@ -79,10 +130,14 @@ class DecimalWatchFaceView extends WatchUi.WatchFace {
|
||||
|
||||
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
|
||||
|
||||
// Determine scale based on mode (5 or 10 hour)
|
||||
var maxValue = useFiveHourMode ? 5.0 : 10.0;
|
||||
var tickCount = useFiveHourMode ? 50 : 100; // 0.1 intervals
|
||||
|
||||
// Draw tick marks at 0.1 intervals (major at whole numbers, minor at 0.1)
|
||||
for (var i = 0; i < 100; i++) {
|
||||
for (var i = 0; i < tickCount; i++) {
|
||||
var decimalValue = i * 0.1;
|
||||
var angle = (decimalValue / 10.0) * 360.0 - 90.0; // -90 to start at top
|
||||
var angle = (decimalValue / maxValue) * 360.0 - 90.0; // -90 to start at top
|
||||
var angleRad = Math.toRadians(angle);
|
||||
|
||||
// Determine if this is a major tick (whole number) or minor tick
|
||||
@ -104,9 +159,29 @@ class DecimalWatchFaceView extends WatchUi.WatchFace {
|
||||
if (isMajor) {
|
||||
var numberX = centerX + (radius - 38) * Math.cos(angleRad);
|
||||
var numberY = centerY + (radius - 38) * Math.sin(angleRad);
|
||||
var numberValue = (i / 10).toNumber(); // Convert to 0-9
|
||||
var numberValue = (i / 10).toNumber(); // Position 0-4 or 0-9
|
||||
|
||||
if (useFiveHourMode) {
|
||||
// 5-hour mode: show different numbers based on morning/afternoon
|
||||
if (isMorning) {
|
||||
// Morning: show 5,1,2,3,4 (5 at top, then 1,2,3,4)
|
||||
if (numberValue == 0) {
|
||||
numberValue = 10; // Display 10 instead of 0
|
||||
numberValue = 5;
|
||||
}
|
||||
// else numberValue stays as 1,2,3,4
|
||||
} else {
|
||||
// Afternoon: show 10,6,7,8,9 (10 at top, then 6,7,8,9)
|
||||
if (numberValue == 0) {
|
||||
numberValue = 10;
|
||||
} else {
|
||||
numberValue = numberValue + 5; // 1->6, 2->7, 3->8, 4->9
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 10-hour mode: show 10 at top instead of 0
|
||||
if (numberValue == 0) {
|
||||
numberValue = 10;
|
||||
}
|
||||
}
|
||||
|
||||
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
|
||||
@ -120,29 +195,67 @@ class DecimalWatchFaceView extends WatchUi.WatchFace {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw center dot
|
||||
dc.fillCircle(centerX, centerY, 5);
|
||||
// Note: Center dot is drawn in drawHand() to ensure it's on top of everything
|
||||
}
|
||||
|
||||
// Draw both hour and minute hands for current decimal time
|
||||
function drawHand(dc, decimalTime) {
|
||||
// Draw hour, minute, and second hands for current decimal time
|
||||
function drawHand(dc, decimalTime, useFiveHourMode, show12HourShadow, clockTime) {
|
||||
var width = dc.getWidth();
|
||||
var height = dc.getHeight();
|
||||
var centerX = width / 2;
|
||||
var centerY = height / 2;
|
||||
var radius = (width < height ? width : height) / 2 - 10;
|
||||
|
||||
// Calculate hour hand (shows which decimal unit 0-9, one rotation per day)
|
||||
var hourAngle = (decimalTime / 10.0) * 360.0 - 90.0;
|
||||
var hourAngleRad = Math.toRadians(hourAngle);
|
||||
// Draw traditional 12-hour shadow hands first (if enabled), so they appear behind
|
||||
if (show12HourShadow) {
|
||||
var hour = clockTime.hour;
|
||||
var minute = clockTime.min;
|
||||
|
||||
// Calculate traditional 12-hour hand angle
|
||||
var traditional12HourAngle = ((hour % 12) + (minute / 60.0)) / 12.0 * 360.0 - 90.0;
|
||||
var traditional12HourLength = radius - 70; // Same as decimal hour hand
|
||||
|
||||
// Calculate traditional 60-minute hand angle
|
||||
var traditional60MinuteAngle = (minute / 60.0) * 360.0 - 90.0;
|
||||
var traditional60MinuteLength = radius - 45; // Same as decimal minute hand
|
||||
|
||||
// Choose colors based on screen type (darker for AMOLED burn-in protection)
|
||||
var shadowGray = useAmoledColors ? VERY_DARK_GRAY : Graphics.COLOR_DK_GRAY;
|
||||
var shadowRed = useAmoledColors ? VERY_DARK_RED : Graphics.COLOR_DK_RED;
|
||||
|
||||
// Draw traditional minute hand (dark gray)
|
||||
drawSingleHand(dc, centerX, centerY, traditional60MinuteAngle, traditional60MinuteLength, 4, shadowGray);
|
||||
|
||||
// Draw traditional hour hand (dark red)
|
||||
drawSingleHand(dc, centerX, centerY, traditional12HourAngle, traditional12HourLength, 6, shadowRed);
|
||||
}
|
||||
|
||||
// Adjust scale based on mode
|
||||
var maxValue = useFiveHourMode ? 5.0 : 10.0;
|
||||
|
||||
// Calculate hour hand (shows which decimal unit)
|
||||
// In 10-hour mode: one rotation per day
|
||||
// In 5-hour mode: two rotations per day
|
||||
var hourAngle = (decimalTime / maxValue) * 360.0 - 90.0;
|
||||
var hourHandLength = radius - 70; // Shorter hand
|
||||
|
||||
// Calculate minute hand (shows fractional part, 10 rotations per day)
|
||||
// Calculate minute hand (shows fractional part)
|
||||
var fractionalPart = decimalTime - decimalTime.toNumber(); // Get decimal part
|
||||
var minuteAngle = fractionalPart * 360.0 - 90.0;
|
||||
var minuteAngleRad = Math.toRadians(minuteAngle);
|
||||
var minuteHandLength = radius - 45; // Longer hand
|
||||
|
||||
// Calculate second hand (shows finer detail)
|
||||
// Only visible in high power mode
|
||||
var multiplier = 100.0; // 0.01 increments
|
||||
var secondFractionalPart = (decimalTime * multiplier) - (decimalTime * multiplier).toNumber();
|
||||
var secondAngle = secondFractionalPart * 360.0 + 90.0;
|
||||
var secondHandLength = radius - 35; // Longest hand
|
||||
|
||||
// Draw second hand first (if in high power mode), so it appears behind other hands
|
||||
if (isHighPowerMode) {
|
||||
drawSingleHand(dc, centerX, centerY, secondAngle, secondHandLength, 2, Graphics.COLOR_YELLOW);
|
||||
}
|
||||
|
||||
// Draw minute hand (longer, thinner)
|
||||
drawSingleHand(dc, centerX, centerY, minuteAngle, minuteHandLength, 4, Graphics.COLOR_WHITE);
|
||||
|
||||
@ -198,4 +311,35 @@ class DecimalWatchFaceView extends WatchUi.WatchFace {
|
||||
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER
|
||||
);
|
||||
}
|
||||
|
||||
// Draw traditional 12-hour time display
|
||||
function drawTraditionalTime(dc, clockTime) {
|
||||
var width = dc.getWidth();
|
||||
var height = dc.getHeight();
|
||||
|
||||
var hour = clockTime.hour;
|
||||
var minute = clockTime.min;
|
||||
|
||||
// Convert to 12-hour format
|
||||
var hour12 = hour % 12;
|
||||
if (hour12 == 0) {
|
||||
hour12 = 12;
|
||||
}
|
||||
|
||||
// Format as HH:MM
|
||||
var timeString = hour12.format("%d") + ":" + minute.format("%02d");
|
||||
|
||||
// Choose color based on screen type (darker for AMOLED burn-in protection)
|
||||
var shadowGray = useAmoledColors ? VERY_DARK_GRAY : Graphics.COLOR_DK_GRAY;
|
||||
|
||||
// Draw at top center in dark gray
|
||||
dc.setColor(shadowGray, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawText(
|
||||
width / 2,
|
||||
height * 0.25,
|
||||
Graphics.FONT_LARGE,
|
||||
timeString,
|
||||
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user