initial commit

This commit is contained in:
2025-10-09 14:37:45 +03:00
commit 0ec07dc014
24 changed files with 791 additions and 0 deletions

View File

@ -0,0 +1,97 @@
import Toybox.Application.Properties;
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.WatchUi;
class IBackground extends Drawable {
var Color as ColorType;
var Hands as Array<IHand> = [];
var Marks as Array<IMark> = [];
typedef BackgroundParams as {
:Identifier as Object,
:Color as ColorType,
:Hands as Array<IHand.HandType>,
};
enum BackgroundStyleType {
SOLID_BACKGROUND,
}
static function getBackground(style as BackgroundStyleType, options as BackgroundParams) as IBackground {
switch (style) {
case SOLID_BACKGROUND:
default:
return new SolidBackground(options);
}
}
function initialize(options as BackgroundParams) {
var identifier = options[:Identifier];
Drawable.initialize({:identifier => identifier});
Color = Properties.getValue(Lang.format("$1$/Background/Color", [identifier])) as ColorType;
for (var seconds = 0; seconds < 60; ++seconds) {
var markType = IMark.getMarkType(seconds);
var markIdentifier = Lang.format("$1$/Marks/$2$", [identifier, markType]);
var markStyle = Properties.getValue(Lang.format("$1$/Type", [markIdentifier])) as IMark.MarkStyleType;
var mark = IMark.getMark(markStyle, markType, {
:Identifier => markIdentifier,
:BackgroundColor => Color,
:Color => Properties.getValue(Lang.format("$1$/Color", [markIdentifier])) as ColorType,
:Seconds => seconds,
:Size => markType == IMark.TERTIARY_MARK ? 0.033 : 0.1,
});
if (mark != null) {
Marks.add(mark);
}
}
var hands = options[:Hands];
for (var i = 0; i < hands.size(); ++i) {
var handType = hands[i];
var handIdentifier = Lang.format("$1$/Hands/$2$", [identifier, handType]);
var handStyle = Properties.getValue(Lang.format("$1$/Type", [handIdentifier])) as IHand.HandStyleType;
var hand = IHand.getHand(handStyle, {
:Identifier => handIdentifier,
:BackgroundColor => Color,
:Color => Properties.getValue(Lang.format("$1$/Color", [handIdentifier])) as ColorType,
:Type => handType,
});
if (hand != null) {
Hands.add(hand);
}
}
}
function draw(dc as Dc) as Void {
drawBackground(dc);
for (var i = 0; i < Marks.size(); ++i) {
Marks[i].draw(dc);
}
var widths = [] as Array<Float>;
var circleColor = null as ColorType;
for (var i = 0; i < Hands.size(); ++i) {
Hands[i].draw(dc);
widths.add(Hands[i].Width);
circleColor = Hands[i].Color;
}
var center = getCenter(dc, [0, 0]);
var circleWidth = max(widths) * min(center);
dc.setColor(circleColor, circleColor);
dc.fillCircle(center[0], center[1], circleWidth);
dc.setColor(Color, Color);
dc.fillCircle(center[0], center[1], circleWidth * 0.5);
}
function drawBackground(dc as Dc) as Void {}
}

View File

@ -0,0 +1,15 @@
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.WatchUi;
class SolidBackground extends IBackground {
function initialize(options as IBackground.BackgroundParams) {
IBackground.initialize(options);
}
function drawBackground(dc as Dc) as Void {
dc.setColor(Graphics.COLOR_TRANSPARENT, Color);
dc.clear();
}
}

21
source/Drawables.mc Normal file
View File

@ -0,0 +1,21 @@
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.Math;
function fillSuperellipse(dc as Dc, start as Point2D, direction as Point2D, length as Float, width as Float) {
var halfWidth = min(start) * width / 2.0;
var perpendicular = [-direction[1], direction[0]] as Point2D;
var end = [start[0] + direction[0] * length, start[1] + direction[1] * length];
// body part
dc.fillPolygon([
[start[0] + perpendicular[0] * halfWidth, start[1] + perpendicular[1] * halfWidth],
[start[0] - perpendicular[0] * halfWidth, start[1] - perpendicular[1] * halfWidth],
[end[0] - perpendicular[0] * halfWidth, end[1] - perpendicular[1] * halfWidth],
[end[0] + perpendicular[0] * halfWidth, end[1] + perpendicular[1] * halfWidth],
]);
// ends
dc.fillCircle(start[0], start[1], halfWidth);
dc.fillCircle(end[0], end[1], halfWidth);
}

7
source/Field.mc Normal file
View File

@ -0,0 +1,7 @@
import Toybox.Graphics;
import Toybox.Lang;
typedef FieldParams as {
:CenterShift as Point2D,
:Radius as Float,
};

26
source/Hands/BatonHand.mc Normal file
View File

@ -0,0 +1,26 @@
import Toybox.Graphics;
import Toybox.Lang;
class BatonHand extends IHand {
function initialize(options as IHand.HandParams) {
IHand.initialize(options);
}
function drawHand(dc as Dc, start as Point2D, length as Float) as Void {
var angle = Math.toRadians(getAngle(null) - 90);
var direction = [Math.cos(angle), Math.sin(angle)] as Point2D;
// body part
dc.setColor(Color, Color);
fillSuperellipse(dc, start, direction, length, Width);
// fill
dc.setColor(BackgroundColor, BackgroundColor);
var holeStart = [
start[0] + direction[0] * length * 0.33,
start[1] + direction[1] * length * 0.33
] as Point2D;
fillSuperellipse(dc, holeStart, direction, length * 0.66, Width * 0.66);
}
}

94
source/Hands/IHand.mc Normal file
View File

@ -0,0 +1,94 @@
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.System;
import Toybox.WatchUi;
class IHand extends Drawable {
var BackgroundColor as ColorType;
var CenterShift as Point2D;
var Color as ColorType;
var Radius as Float;
var Type as HandType;
var Width as Float;
typedef HandParams as {
:Identifier as Object,
:Color as ColorType,
:Type as HandType,
:Width as Float,
:Field as FieldParams,
};
enum HandType {
OTHER_HAND = 0,
HOURS_HAND = 1,
MINUTES_HAND = 2,
SECONDS_HAND = 3,
}
enum HandStyleType {
EMPTY_HANDS = 0,
SIMPLE_HANDS = 1,
BATON_HANDS = 2,
}
static function getHand(style as HandStyleType, options as HandParams) as IHand? {
switch (style) {
case SIMPLE_HANDS:
return new SimpleHand(options);
case BATON_HANDS:
return new BatonHand(options);
case EMPTY_HANDS:
default:
return null;
}
}
function initialize(options as HandParams) {
Drawable.initialize({:identifier => options[:Identifier]});
// scene
var field = getOrElse(options[:Field], {}) as FieldParams;
CenterShift = getOrElse(field[:CenterShift], [0.0, 0.0]);
Radius = 0.95 * getOrElse(field[:Radius], 1.0);
// properties
BackgroundColor = options[:BackgroundColor];
Color = options[:Color];
Type = options[:Type];
Width = Radius * getOrElse(options[:Witdh], 0.05);
}
function draw(dc as Dc) as Void {
var center = getCenter(dc, CenterShift);
var length = Radius * min(center) * getLenght(Type);
drawHand(dc, center, length);
}
function drawHand(dc as Dc, start as Point2D, length as Float) as Void {}
function getAngle(angle as Float?) as Float? {
var now = System.getClockTime();
switch (Type) {
case HOURS_HAND:
return (now.hour % 12 + now.min / 60.0) * 30.0;
case MINUTES_HAND:
return now.min * 6.0;
case SECONDS_HAND:
return now.sec * 6.0;
case OTHER_HAND:
default:
return angle;
}
}
function getLenght(handType as HandType) as Float {
switch (handType) {
case HOURS_HAND:
return 0.7;
default:
return 1.0;
}
}
}

View File

@ -0,0 +1,18 @@
import Toybox.Graphics;
import Toybox.Lang;
class SimpleHand extends IHand {
function initialize(options as IHand.HandParams) {
IHand.initialize(options);
}
function drawHand(dc as Dc, start as Point2D, length as Float) as Void {
var angle = Math.toRadians(getAngle(null) - 90);
dc.setColor(Color, Color);
dc.drawLine(
start[0], start[1],
start[0] + length * Math.cos(angle), start[1] + length * Math.sin(angle)
);
}
}

View File

@ -0,0 +1,29 @@
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.WatchUi;
class ArabicMark extends IMark {
var Font as FontType;
function initialize(options as IMark.MarkParams) {
IMark.initialize(options);
Font = getOrElse(options[:Font], Graphics.FONT_SMALL);
}
function drawMark(dc as Dc, start as Point2D, length as Float) as Void {
var angle = getAngle();
var text = secondsToText();
var dimentions = dc.getTextDimensions(text, Font);
dc.setColor(Color, Graphics.COLOR_TRANSPARENT);
dc.drawText(start[0] + length * InnerRadius * Math.cos(angle) - dimentions[0] / 2.0,
start[1] + length * InnerRadius * Math.sin(angle) - dimentions[1] / 2.0,
Font, text, Graphics.TEXT_JUSTIFY_LEFT);
}
function secondsToText() as String? {
return getHours().toString();
}
}

19
source/Marks/DotMark.mc Normal file
View File

@ -0,0 +1,19 @@
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.WatchUi;
class DotMark extends IMark {
function initialize(options as IMark.MarkParams) {
IMark.initialize(options);
}
function drawMark(dc as Dc, start as Point2D, length as Float) as Void {
var angle = getAngle();
dc.setColor(Color, Graphics.COLOR_TRANSPARENT);
dc.fillCircle(start[0] + length * InnerRadius * Math.cos(angle),
start[1] + length * InnerRadius * Math.sin(angle),
length * Size / 2.0);
}
}

View File

@ -0,0 +1,26 @@
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.WatchUi;
class DoubleLineMark extends IMark {
var Offset as Float = Math.toRadians(1.0);
function initialize(options as IMark.MarkParams) {
IMark.initialize(options);
}
function drawMark(dc as Dc, start as Point2D, length as Float) as Void {
var angle = getAngle();
dc.setColor(Color, Color);
dc.drawLine(start[0] + length * InnerRadius * Math.cos(angle - Offset),
start[1] + length * InnerRadius * Math.sin(angle - Offset),
start[0] + length * Radius * Math.cos(angle - Offset),
start[1] + length * Radius * Math.sin(angle - Offset));
dc.drawLine(start[0] + length * InnerRadius * Math.cos(angle + Offset),
start[1] + length * InnerRadius * Math.sin(angle + Offset),
start[0] + length * Radius * Math.cos(angle + Offset),
start[1] + length * Radius * Math.sin(angle + Offset));
}
}

122
source/Marks/IMark.mc Normal file
View File

@ -0,0 +1,122 @@
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.WatchUi;
class IMark extends Drawable {
var BackgroundColor as ColorType;
var CenterShift as Point2D;
var Color as ColorType;
var InnerRadius as Float;
var Radius as Float;
var Seconds as Number;
var Size as Float;
typedef MarkParams as {
:Identifier as Object,
:Color as ColorType,
:Font as FontType, // not used
:Seconds as Number,
:Size as Float,
:Field as FieldParams,
};
enum MarkType {
START_MARK = 0,
PRIMARY_MARK = 1,
SECONDARY_MARK = 2,
TERTIARY_MARK = 3,
}
enum MarkStyleType {
EMPTY_MARK = 0,
LINE_MARK = 1,
DOUBLE_LINE_MARK = 2,
DOT_MARK = 3,
ARABIC_MARK = 4,
ROMAN_MARK = 5,
}
static function getMark(style as MarkStyleType, type as MarkType, options as MarkParams) as IMark? {
switch (style) {
case LINE_MARK:
return new LineMark(options);
case DOUBLE_LINE_MARK:
return new DoubleLineMark(options);
case DOT_MARK:
return new DotMark(options);
case ARABIC_MARK:
return type == TERTIARY_MARK ? null : new ArabicMark(options);
case ROMAN_MARK:
return type == TERTIARY_MARK ? null : new RomanMark(options);
case EMPTY_MARK:
default:
return null;
}
}
static function getMarkType(seconds as Number) as MarkType {
switch (seconds) {
case 0:
return START_MARK;
case 15:
case 30:
case 45:
return PRIMARY_MARK;
case 5:
case 10:
case 20:
case 25:
case 35:
case 40:
case 50:
case 55:
return SECONDARY_MARK;
default:
return TERTIARY_MARK;
}
}
function initialize(options as MarkParams) {
Drawable.initialize({:identifier => options[:Identifier]});
// scene
var field = getOrElse(options[:Field], {}) as FieldParams;
CenterShift = getOrElse(field[:CenterShift], [0.0, 0.0]);
Radius = getOrElse(field[:Radius], 1.0);
// properties
BackgroundColor = options[:BackgroundColor];
Color = options[:Color];
Seconds = options[:Seconds];
Size = options[:Size];
// calculated
InnerRadius = Radius * (1 - Size);
}
function draw(dc as Dc) as Void {
var center = getCenter(dc, CenterShift);
var length = min(center);
drawMark(dc, center, length);
}
function drawMark(dc as Dc, start as Point2D, length as Float) as Void {}
function getAngle() as Float {
return Math.toRadians(Seconds * 6.0 - 90);
}
function getHours() as Number? {
if (Seconds % 5 != 0) {
return null;
}
var hours = Seconds / 5;
if (hours == 0) {
return 12;
}
return hours;
}
}

20
source/Marks/LineMark.mc Normal file
View File

@ -0,0 +1,20 @@
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.WatchUi;
class LineMark extends IMark {
function initialize(options as IMark.MarkParams) {
IMark.initialize(options);
}
function drawMark(dc as Dc, start as Point2D, length as Float) as Void {
var angle = getAngle();
dc.setColor(Color, Color);
dc.drawLine(start[0] + length * InnerRadius * Math.cos(angle),
start[1] + length * InnerRadius * Math.sin(angle),
start[0] + length * Radius * Math.cos(angle),
start[1] + length * Radius * Math.sin(angle));
}
}

41
source/Marks/RomanMark.mc Normal file
View File

@ -0,0 +1,41 @@
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.WatchUi;
class RomanMark extends ArabicMark {
function initialize(options as IMark.MarkParams) {
ArabicMark.initialize(options);
}
function secondsToText() as String? {
switch (getHours()) {
case 1:
return "I";
case 2:
return "II";
case 3:
return "III";
case 4:
return "IV";
case 5:
return "V";
case 6:
return "VI";
case 7:
return "VII";
case 8:
return "VIII";
case 9:
return "IX";
case 10:
return "X";
case 11:
return "XI";
case 12:
return "XII";
default:
return null;
}
}
}

47
source/Utils.mc Normal file
View File

@ -0,0 +1,47 @@
import Toybox.Lang;
import Toybox.Graphics;
// generic function for arrays
function find(source as Array, searchFunction as Method(left, right) as Boolean) {
if (source == null || source.size() == 0) {
return null;
}
var result = source[0];
for (var i = 1; i < source.size(); ++i) {
if (searchFunction.invoke(result, source[i])) {
result = source[i];
}
}
return result;
}
// no types here, because this is generic, which are not supported by language
function getOrElse(value, defaultValue) {
if (value == null) {
return defaultValue;
} else {
return value;
}
}
function _max(left as Numeric, right as Numeric) as Boolean {
return left < right;
}
function max(source as Array<Numeric>) as Numeric {
return find(source, new Method($, :_max));
}
function _min(left as Numeric, right as Numeric) as Boolean {
return left > right;
}
function min(source as Array<Numeric>) as Numeric {
return find(source, new Method($, :_min));
}
function getCenter(dc as Dc, shift as Point2D) as Point2D {
return [dc.getWidth() / 2.0 + shift[0], dc.getHeight() / 2.0 + shift[1]];
}

33
source/wfApp.mc Normal file
View File

@ -0,0 +1,33 @@
import Toybox.Application;
import Toybox.Lang;
import Toybox.WatchUi;
class wfApp extends Application.AppBase {
function initialize() {
AppBase.initialize();
}
// onStart() is called on application start up
function onStart(state as Dictionary?) as Void {
}
// onStop() is called when your application is exiting
function onStop(state as Dictionary?) as Void {
}
// Return the initial view of your application here
function getInitialView() as [Views] or [Views, InputDelegates] {
return [ new wfView() ];
}
// New app settings have been received so trigger a UI update
function onSettingsChanged() as Void {
WatchUi.requestUpdate();
}
}
function getApp() as wfApp {
return Application.getApp() as wfApp;
}

49
source/wfView.mc Normal file
View File

@ -0,0 +1,49 @@
import Toybox.Application;
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.System;
import Toybox.WatchUi;
class wfView extends WatchUi.WatchFace {
private var background as IBackground;
function initialize() {
WatchFace.initialize();
background = IBackground.getBackground(IBackground.SOLID_BACKGROUND, {
:Identifier => "Main",
:Hands => [IHand.HOURS_HAND, IHand.MINUTES_HAND, IHand.SECONDS_HAND],
});
}
// Load your resources here
function onLayout(dc as Dc) as Void {
}
// Called when this View is brought to the foreground. Restore
// the state of this View and prepare it to be shown. This includes
// loading resources into memory.
function onShow() as Void {
}
// Update the view
function onUpdate(dc as Dc) as Void {
background.draw(dc);
}
// Called when this View is removed from the screen. Save the
// state of this View here. This includes freeing resources from
// memory.
function onHide() as Void {
}
// The user has just looked at their watch. Timers and animations may be started here.
function onExitSleep() as Void {
}
// Terminate any active timers and prepare for slow updates.
function onEnterSleep() as Void {
}
}