Refactor watch to use modules & webpack! :)

master
Auri 2021-09-13 01:19:38 -07:00
parent 7e0acb45e9
commit 3140f003da
19 changed files with 4523 additions and 537 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules
Focus.wgt

1
watch/.buildignore Normal file
View File

@ -0,0 +1 @@
webpack.ts,tsconfig.json,package.json,package-lock.json,.eslintrc.js,src,node_modules,

163
watch/.eslintrc.js Normal file
View File

@ -0,0 +1,163 @@
/*
👋 Hi! This file was autogenerated by tslint-to-eslint-config.
https://github.com/typescript-eslint/tslint-to-eslint-config
It represents the closest reasonable ESLint configuration to this
project's original TSLint configuration.
We recommend eventually switching this configuration to extend from
the recommended rulesets in typescript-eslint.
https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md
Happy linting! 💖
*/
module.exports = {
"env": {
"browser": true
},
"extends": [
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/dot-notation": "error",
"@typescript-eslint/indent": [
"error",
"tab",
{
"CallExpression": {
"arguments": 1
},
"FunctionDeclaration": {
"parameters": 1
},
"FunctionExpression": {
"parameters": 1
}
}
],
"@typescript-eslint/member-delimiter-style": [
"error",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/no-use-before-define": "error",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/quotes": [
"error",
"single"
],
"@typescript-eslint/semi": [
"error",
"always"
],
"@typescript-eslint/type-annotation-spacing": "error",
"brace-style": [
"error",
"stroustrup",
{ "allowSingleLine": true }
],
"comma-dangle": "error",
"curly": "off",
"default-case": "error",
"eol-last": "error",
"eqeqeq": [
"error",
"smart"
],
"guard-for-in": "error",
"id-blacklist": [
"error",
"any",
"Number",
"number",
"String",
"string",
"Boolean",
"boolean",
"Undefined",
"undefined"
],
"id-match": "error",
"max-len": [
"error", {
"code": 150
}
],
"no-bitwise": "error",
"no-caller": "error",
"no-console": [
"error",
{
"allow": [
"log",
"dirxml",
"warn",
"error",
"dir",
"timeLog",
"assert",
"clear",
"count",
"countReset",
"group",
"groupCollapsed",
"groupEnd",
"table",
"Console",
"markTimeline",
"profile",
"profileEnd",
"timeline",
"timelineEnd",
"timeStamp",
"context"
]
}
],
"no-debugger": "error",
"no-empty": "error",
"no-eval": "error",
"no-fallthrough": "error",
"no-new-wrappers": "error",
"no-redeclare": "error",
"no-trailing-spaces": [
"error", {
"skipBlankLines": true
}
],
"no-unused-labels": "error",
"no-var": "error",
"radix": "error",
"spaced-comment": [
"error",
"always",
{
"markers": [
"/"
]
}
]
}
};

Binary file not shown.

View File

@ -1 +0,0 @@
1.0.5

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,9 @@
<?xml version='1.0' encoding='UTF-8'?>
<widget xmlns:tizen='http://tizen.org/ns/widgets' xmlns='http://www.w3.org/ns/widgets' id='http://yourdomain/Focus' version='1.0.0' viewmodes='maximized'>
<tizen:application id='kFVD8WK8ro.Focus' package='kFVD8WK8ro' required_version='2.3.1' ambient_support='enable'/>
<tizen:category name='http://tizen.org/category/wearable_clock'/>
<!-- <tizen:category name='http://tizen.org/category/wearable_clock'/> -->
<feature name='http://tizen.org/feature/screen.shape.circle'/>
<feature name='http://tizen.org/feature/screen.size.all'/>
<feature name='http://tizen.org/feature/calendar'/>
<feature name='http://tizen.org/feature/calendar.read'/>
<feature name='http://tizen.org/feature/calendar.write'/>
<content src='index.html'/>
<icon src='icon.png'/>
<name>Focus</name>

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no' />
<title>AmbientWatch</title>
<title>Focus</title>
<style>
html, body {
margin: 0;
@ -13,9 +13,9 @@
}
canvas {
width: 100%;
height: 100%;
margin: auto;
width: 100%;
height: 100%;
margin: auto;
}
</style>
</head>

3764
watch/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"description": "Tizen application for the Focus Watch Face.",
"main": "build/Main.js",
"scripts": {
"dev": "nodemon"
"dev": "webpack --watch --progress --config webpack.ts"
},
"repository": {
"type": "git",
@ -31,9 +31,22 @@
},
"homepage": "https://github.com/Aurailus/Focus#readme",
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/preset-env": "^7.15.6",
"@babel/preset-typescript": "^7.15.0",
"@types/tizen-common-web": "^2.0.1",
"@types/webpack": "^5.28.0",
"@typescript-eslint/eslint-plugin": "^4.31.0",
"@typescript-eslint/parser": "^4.31.0",
"babel-loader": "^8.2.2",
"eslint": "^7.32.0",
"fork-ts-checker-webpack-plugin": "^6.3.3",
"nodemon": "^2.0.12",
"ts-node": "^10.2.1",
"typescript": "^4.4.3"
"tslib": "^2.3.1",
"typescript": "^4.4.3",
"webpack": "^5.52.1",
"webpack-cli": "^4.8.0",
"webpack-merge": "^5.8.0"
}
}

273
watch/src/Artist.ts Normal file
View File

@ -0,0 +1,273 @@
import { Event } from './Events';
import { degToRad } from './Util';
/** The width of events. */
const EVENT_WIDTH = 22;
/** The distance inside the event that the title should be drawn. */
const TITLE_SPACING = 2;
/** The text size of the event title. */
const TITLE_SIZE = 16;
// TODO: placeholders
const COLOR_LIGHT_DIM = 'rgba(65, 199, 232, 0.25)';
/**
* Handles drawing shapes used by the watch face.
* init() loads resources, and **must** be awaited before using any operations.
*/
export default class Artist {
readonly radius: number;
readonly ctx: CanvasRenderingContext2D;
private glowCtx: CanvasRenderingContext2D;
private glowImg: HTMLImageElement;
/**
* Initializes an Artist to the canvas or canvas context provided.
*
* @param canvas - The canvas or canvas context to bind the artist to.
*/
constructor(canvas: HTMLCanvasElement | CanvasRenderingContext2D) {
if ('getContext' in canvas) this.ctx = canvas.getContext('2d')!;
else this.ctx = canvas;
this.radius = this.ctx.canvas.width / 2;
const glowCanvas = document.createElement('canvas');
glowCanvas.width = 128;
glowCanvas.height = 128;
this.glowCtx = glowCanvas.getContext('2d')!;
this.glowImg = null as any;
}
/**
* Loads resources needed for certain draw operations.
*
* @returns a promise indicating that the loading is complete.
*/
init(): Promise<void> {
return new Promise<void>(resolve => {
this.glowImg = document.createElement('img');
this.glowImg.onload = () => resolve();
this.glowImg.src = '../res/glow.png';
});
}
/**
* Clears the canvas for a new frame.
*/
clear() {
const { ctx, radius: canvRadius } = this;
ctx.clearRect(0, 0, canvRadius * 2, canvRadius * 2);
}
/**
* Draws a circle on the canvas.
*
* @param radians - The angle in radians to draw the circle at, starting at the top and moving clockwise.
* @param dist - The distance from the center of the canvas to draw the circle at.
* @param radius - The radius of the circle to draw.
* @param color - The color to draw the circle with.
*/
circle(radians: number, dist: number, radius: number, color: string) {
const { ctx, radius: canvRadius } = this;
ctx.save();
ctx.translate(canvRadius, canvRadius);
ctx.rotate(radians);
ctx.beginPath();
ctx.arc(dist, 0, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
ctx.restore();
}
/**
* Draws a line on the canvas.
*
* @param radians - The angle in radians to draw the line at, starting at the top and moving clockwise.
* @param startDist - The distance from the center of the canvas to begin the line at.
* @param endDist - The distance from the center of the canvas to end the line at.
* @param width - The width of the line to draw.
* @param color - The color to draw the line with.
*/
line(radians: number, startDist: number, endDist: number, width: number, color: string) {
const { ctx, radius: canvRadius } = this;
ctx.save();
ctx.translate(canvRadius, canvRadius);
ctx.rotate(radians);
ctx.beginPath();
ctx.lineWidth = width;
ctx.strokeStyle = color;
ctx.moveTo(0, startDist);
ctx.lineTo(0, endDist);
ctx.stroke();
ctx.closePath();
ctx.restore();
}
/**
* Draws a rounded rectangle on the canvas.
*
* @param radians - The angle in radians to draw the rounded rectangle at, starting at the top and moving clockwise.
* @param startDist - The distance from the center of the canvas to begin the rounded rectangle at.
* @param endDist - The distance from the center of the canvas to end the rounded rectangle at.
* @param width - The width of the rounded rectangle to draw.
* @param fill - The color to fill the rounded rectangle with. undefined will result in no fill.
* @param stroke - The color to trace the rounded rectangle with. undefined will result in no stroke.
* @param strokeWidth - The width of the stroke for the rounded rectangle with. undefined will result in no stroke.
*/
rounded(radians: number, startDist: number, endDist: number, width: number,
fill?: string, stroke?: string, strokeWidth?: number ) {
const { ctx, radius: canvRadius } = this;
ctx.save();
ctx.translate(canvRadius, canvRadius);
ctx.rotate(radians);
ctx.beginPath();
ctx.moveTo(-width / 2, startDist);
ctx.lineTo(-width / 2, endDist);
ctx.quadraticCurveTo(-width / 2, endDist + width / 1.5, 0, endDist + width / 1.5);
ctx.quadraticCurveTo(width / 2, endDist + width / 1.5, width / 2, endDist);
ctx.lineTo(width / 2, startDist);
ctx.quadraticCurveTo(width / 2, startDist - width / 1.5, 0, startDist - width / 1.5);
ctx.quadraticCurveTo(-width / 2, startDist - width / 1.5, -width / 2, startDist);
ctx.closePath();
if (fill) {
ctx.fillStyle = fill;
ctx.fill();
}
if (stroke && strokeWidth) {
ctx.strokeStyle = stroke;
ctx.lineWidth = strokeWidth;
ctx.stroke();
}
ctx.restore();
}
/**
* Draws a glow on the canvas.
*
* @param radians - The angle in radians to draw the glow at, starting at the top and moving clockwise.
* @param dist - The distance from the center of the canvas to draw the glow at.
* @param scale - A scale multiplier for the glow's size. A value of 1 results in a size 66% of the canvas size.
* @param color - The color to draw the glow with.
*/
glow(radians: number, dist: number, scale: number, color: string) {
const { ctx, glowCtx, glowImg, radius: canvRadius } = this;
const offsetX = Math.cos(radians) * dist;
const offsetY = Math.sin(radians) * dist;
glowCtx.fillStyle = color;
glowCtx.globalCompositeOperation = 'source-over';
glowCtx.fillRect(0, 0, glowCtx.canvas.width, glowCtx.canvas.height);
glowCtx.globalCompositeOperation = 'destination-in';
glowCtx.drawImage(glowImg, 0, 0);
const size = (canvRadius * 1.5) * scale;
ctx.drawImage(glowCtx.canvas,
canvRadius + offsetX - size / 2, canvRadius + offsetY - size / 2,
size, size);
}
/**
* Draws a calendar event on the canvas.
*
* @param event - The event to draw.
* @param dist - The distance from the center of the canvas to draw the event at.
*/
event(event: Event, dist: number) {
const { ctx, radius: canvRadius } = this;
ctx.save();
ctx.translate(canvRadius, canvRadius);
const startAngle = degToRad(((event.start.getHours() % 12) +
event.start.getMinutes() / 60) / 12 * 360 + EVENT_WIDTH / 6 - 90);
const endAngle = degToRad(((event.end.getHours() % 12) +
event.end.getMinutes() / 60) / 12 * 360 - EVENT_WIDTH / 8 - 90);
ctx.beginPath();
ctx.fillStyle = COLOR_LIGHT_DIM;
ctx.arc(0, 0, dist, startAngle, endAngle, false);
let controlPosX = Math.cos(endAngle + degToRad(4.25)) * dist;
let controlPosY = Math.sin(endAngle + degToRad(4.25)) * dist;
let endPosX = Math.cos(endAngle + degToRad(4.25)) * (dist - EVENT_WIDTH / 2);
let endPosY = Math.sin(endAngle + degToRad(4.25)) * (dist - EVENT_WIDTH / 2);
ctx.quadraticCurveTo(controlPosX, controlPosY, endPosX, endPosY);
controlPosX = Math.cos(endAngle + degToRad(4.25)) * (dist - EVENT_WIDTH);
controlPosY = Math.sin(endAngle + degToRad(4.25)) * (dist - EVENT_WIDTH);
endPosX = Math.cos(endAngle) * (dist - EVENT_WIDTH);
endPosY = Math.sin(endAngle) * (dist - EVENT_WIDTH);
ctx.quadraticCurveTo(controlPosX, controlPosY, endPosX, endPosY);
ctx.arc(0, 0, dist - EVENT_WIDTH, endAngle, startAngle, true);
controlPosX = Math.cos(startAngle - degToRad(4.25)) * (dist - EVENT_WIDTH);
controlPosY = Math.sin(startAngle - degToRad(4.25)) * (dist - EVENT_WIDTH);
endPosX = Math.cos(startAngle - degToRad(4.25)) * (dist - EVENT_WIDTH / 2);
endPosY = Math.sin(startAngle - degToRad(4.25)) * (dist - EVENT_WIDTH / 2);
ctx.quadraticCurveTo(controlPosX, controlPosY, endPosX, endPosY);
controlPosX = Math.cos(startAngle - degToRad(4.25)) * dist;
controlPosY = Math.sin(startAngle - degToRad(4.25)) * dist;
endPosX = Math.cos(startAngle) * dist;
endPosY = Math.sin(startAngle) * dist;
ctx.quadraticCurveTo(controlPosX, controlPosY, endPosX, endPosY);
ctx.fill();
ctx.closePath();
ctx.restore();
this.circle(startAngle, dist - EVENT_WIDTH / 2, EVENT_WIDTH / 2 - 4, event.color);
ctx.font = `900 ${TITLE_SIZE}px Arial sans-serif`;
ctx.fillStyle = '#fff';
ctx.textBaseline = 'top';
ctx.textAlign = 'center';
let currentAngle = startAngle + degToRad(96);
for (let i = 0; i < event.title.length; i++) {
ctx.save();
ctx.translate(canvRadius, canvRadius);
ctx.rotate(currentAngle);
ctx.fillText(event.title[i], 0, -dist + TITLE_SPACING);
ctx.restore();
if (i < event.title.length - 1)
currentAngle += ctx.measureText(event.title[i]).width * 0.0065 / 2
+ ctx.measureText(event.title[i + 1]).width * 0.0065 / 2;
}
}
}

29
watch/src/Events.ts Normal file
View File

@ -0,0 +1,29 @@
/**
* A calendar event.
*/
export interface Event {
start: Date;
end: Date;
title: string;
color: string;
}
/**
* Returns the current day's calendar events.
*/
export function getEvents(): Event[] {
const createDate = (hour: number, minute: number) => {
const date = new Date();
date.setHours(hour, minute, 0);
return date;
};
return [
{ start: createDate(9, 30), end: createDate(11, 20), color: '#59acff', title: 'PAAS' },
{ start: createDate(12, 30), end: createDate(1, 20), color: '#59acff', title: 'STAT' },
{ start: createDate(1, 30), end: createDate(2, 20), color: '#59acff', title: 'MATH' }
];
}

View File

@ -1,310 +1,20 @@
let theme = {
/** 0: none, 1: quarter, 2: hours, 3: hours + minute */
notches: 3,
/** False: don't display, True: display */
events: true,
/** False: don't show minute hand, True: show minute hand */
minutes: true,
/** False: don't show center glow, True: show center glow */
glow: true,
/** Center glow color. */
glow_a: '#4294ff',
/** Moving glow 1 color. */
glow_b: 'rgba(5, 124, 242, 0.6)',
/** Moving glow 2 color. */
glow_c: 'rgba(198, 0, 237, 1)',
// /** Center glow color. */
// glow_a: '#e838ff',
// /** Moving glow 1 color. */
// glow_b: 'rgba(255, 38, 56, 0.6)',
// /** Moving glow 2 color. */
// glow_c: 'rgba(179, 38, 255, 1)',
}
import Artist from './Artist';
import Watch from './Watch';
let image_loaded = false;
let window_loaded = false;
/**
* Main entrypoint to the watchface.
*/
window.onload = () => {
window_loaded = true;
if (image_loaded) init();
};
(async () => {
await new Promise<void>((resolve) =>
window.onload = () => resolve());
const glow_image = document.createElement('img');
glow_image.src = 'res/glow.png';
glow_image.onload = () => {
image_loaded = true;
if (window_loaded) init();
};
const glow_canvas = document.createElement('canvas');
glow_canvas.width = 128;
glow_canvas.height = 128;
const glow_ctx = glow_canvas.getContext('2d')!;
function init() {
const canvas = document.querySelector('canvas')!;
const ctx = canvas.getContext('2d')!;
canvas.width = document.body.clientWidth;
canvas.height = canvas.width;
const clockRadius = document.body.clientWidth / 2;
let rot = 0;
let ambient = false;
const artist = new Artist(canvas);
await artist.init();
const EVENT_WIDTH = 22;
let EVENT_BUFFER = theme.notches === 0 ? 2 : 14;
const TITLE_BUFFER = 2;
const TITLE_SIZE = 16;
const NOTCH_BUFFER = 2;
const DATE_SIZE = 20;
const COLOR_LIGHT_DIM = 'rgba(65, 199, 232, 0.15)';
const COLOR_LIGHT_OVERLAY = 'rgba(65, 199, 232, 0.33)';
const COLOR_MINUTE_HAND = 'rgba(65, 199, 232, 0.8)';
const degToRad = (deg: number) => deg * (Math.PI / 180);
const createDate = (hour: number, minute: number) => {
const date = new Date();
date.setHours(hour, minute, 0);
return date;
};
const events = [
{ start: createDate(9, 30), end: createDate(11, 20), color: '#33B679', title: 'PAAS' },
{ start: createDate(12, 30), end: createDate(1, 20), color: '#33B679', title: 'STAT' },
{ start: createDate(1, 30), end: createDate(2, 20), color: '#33B679', title: 'MATH' }
// { start: createDate(9, 30), end: createDate(11, 20), color: '#59acff', title: 'Work' },
// { start: createDate(12, 30), end: createDate(1, 20), color: '#59acff', title: 'Walk' },
// { start: createDate(1, 30), end: createDate(2, 20), color: '#59acff', title: 'Bike' }
];
function getTime() {
try { return tizen.time.getCurrentDateTime(); }
catch (e) { return new Date(); }
}
function drawCircle(angle: number, distance: number, width: number, color: string) {
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(angle)
ctx.beginPath();
ctx.arc(distance, 0, width / 2, 0, 2 * Math.PI, false);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
ctx.restore();
}
// function drawLine(startDistance, endDistance, angle, width, color) {
// ctx.save();
// ctx.translate(canvas.width / 2, canvas.height / 2);
// ctx.rotate(degToRad(angle + 90));
// ctx.beginPath();
// ctx.lineWidth = width;
// ctx.strokeStyle = color;
// ctx.moveTo(startDistance, 0);
// ctx.lineTo(endDistance, 0);
// ctx.stroke();
// ctx.closePath();
// ctx.restore();
// }
function drawRoundedRect(startDistance: number, endDistance: number, angle: number, width: number,
fill?: string, stroke?: string, strokeWidth?: number) {
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(-width / 2, startDistance);
ctx.lineTo(-width / 2, endDistance);
ctx.quadraticCurveTo(-width / 2, endDistance + width / 1.5, 0, endDistance + width / 1.5);
ctx.quadraticCurveTo(width / 2, endDistance + width / 1.5, width / 2, endDistance);
ctx.lineTo(width / 2, startDistance);
ctx.quadraticCurveTo(width / 2, startDistance - width / 1.5, 0, startDistance - width / 1.5);
ctx.quadraticCurveTo(-width / 2, startDistance - width / 1.5, -width / 2, startDistance);
ctx.closePath();
if (fill) {
ctx.fillStyle = fill;
ctx.fill();
}
if (stroke && strokeWidth) {
ctx.strokeStyle = stroke;
ctx.lineWidth = strokeWidth;
ctx.stroke();
}
ctx.restore()
}
function drawEvent(event: any) {
const startAngle = degToRad(((event.start.getHours() % 12) +
event.start.getMinutes() / 60) / 12 * 360 + EVENT_WIDTH / 6 - 90);
const endAngle = degToRad(((event.end.getHours() % 12) +
event.end.getMinutes() / 60) / 12 * 360 - EVENT_WIDTH / 8 - 90);
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.beginPath();
ctx.fillStyle = COLOR_LIGHT_DIM;
ctx.arc(0, 0, clockRadius - EVENT_BUFFER, startAngle, endAngle, false);
let controlPosX = Math.cos(endAngle + degToRad(4.25)) * (clockRadius - EVENT_BUFFER);
let controlPosY = Math.sin(endAngle + degToRad(4.25)) * (clockRadius - EVENT_BUFFER);
let endPosX = Math.cos(endAngle + degToRad(4.25)) * (clockRadius - EVENT_BUFFER - EVENT_WIDTH / 2);
let endPosY = Math.sin(endAngle + degToRad(4.25)) * (clockRadius - EVENT_BUFFER - EVENT_WIDTH / 2);
ctx.quadraticCurveTo(controlPosX, controlPosY, endPosX, endPosY);
controlPosX = Math.cos(endAngle + degToRad(4.25)) * (clockRadius - EVENT_BUFFER - EVENT_WIDTH);
controlPosY = Math.sin(endAngle + degToRad(4.25)) * (clockRadius - EVENT_BUFFER - EVENT_WIDTH);
endPosX = Math.cos(endAngle) * (clockRadius - EVENT_BUFFER - EVENT_WIDTH);
endPosY = Math.sin(endAngle) * (clockRadius - EVENT_BUFFER - EVENT_WIDTH);
ctx.quadraticCurveTo(controlPosX, controlPosY, endPosX, endPosY);
ctx.arc(0, 0, clockRadius - EVENT_BUFFER - EVENT_WIDTH, endAngle, startAngle, true);
controlPosX = Math.cos(startAngle - degToRad(4.25)) * (clockRadius - EVENT_BUFFER - EVENT_WIDTH);
controlPosY = Math.sin(startAngle - degToRad(4.25)) * (clockRadius - EVENT_BUFFER - EVENT_WIDTH);
endPosX = Math.cos(startAngle - degToRad(4.25)) * (clockRadius - EVENT_BUFFER - EVENT_WIDTH / 2);
endPosY = Math.sin(startAngle - degToRad(4.25)) * (clockRadius - EVENT_BUFFER - EVENT_WIDTH / 2);
ctx.quadraticCurveTo(controlPosX, controlPosY, endPosX, endPosY);
controlPosX = Math.cos(startAngle - degToRad(4.25)) * (clockRadius - EVENT_BUFFER);
controlPosY = Math.sin(startAngle - degToRad(4.25)) * (clockRadius - EVENT_BUFFER);
endPosX = Math.cos(startAngle) * (clockRadius - EVENT_BUFFER);
endPosY = Math.sin(startAngle) * (clockRadius - EVENT_BUFFER);
ctx.quadraticCurveTo(controlPosX, controlPosY, endPosX, endPosY);
ctx.fill();
ctx.closePath();
ctx.restore();
drawCircle(startAngle, clockRadius - EVENT_BUFFER - EVENT_WIDTH / 2, EVENT_WIDTH - 8, event.color);
ctx.font = `900 ${TITLE_SIZE}px Arial sans-serif`;
ctx.fillStyle = '#fff';
ctx.textBaseline = 'top';
ctx.textAlign = 'center';
let currentAngle = startAngle + degToRad(96);
for (let i = 0; i < event.title.length; i++) {
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(currentAngle);
ctx.fillText(event.title[i], 0, -(clockRadius - EVENT_BUFFER - TITLE_BUFFER));
ctx.restore();
if (i < event.title.length - 1)
currentAngle += ctx.measureText(event.title[i]).width * 0.0065 / 2
+ ctx.measureText(event.title[i + 1]).width * 0.0065 / 2;
}
}
function drawGlow(offsetX: number, offsetY: number, scaleMult: number, color: string) {
glow_ctx.fillStyle = color;
glow_ctx.globalCompositeOperation = 'source-over';
glow_ctx.fillRect(0, 0, glow_canvas.width, glow_canvas.height);
glow_ctx.globalCompositeOperation = 'destination-in';
glow_ctx.drawImage(glow_image, 0, 0);
const scale = 240 * scaleMult;
ctx.drawImage(glow_canvas,
canvas.width / 2 + offsetX - scale / 2,
canvas.height / 2 + offsetY - scale / 2,
scale, scale);
}
function drawWatchFace() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const time = getTime();
ctx.font = `900 ${DATE_SIZE}px Arial sans-serif`;
ctx.fillStyle = '#aaa';
ctx.textBaseline = 'bottom';
ctx.textAlign = 'center';
ctx.fillText(`${['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'][time.getDay()]} ${time.getDate()}`,
clockRadius, clockRadius + clockRadius / 1.75);
ctx.fillStyle = '#666'
ctx.fillRect(clockRadius - 1 * DATE_SIZE, clockRadius + clockRadius / 1.75 + 2, 2 * DATE_SIZE, 2);
if (!ambient) {
rot = (rot + 1) % 360;
let scaleA = 1 + (Math.sin(degToRad(rot + 180)) / 6);
let scaleB = 1 + (Math.cos(degToRad(rot)) / 6);
let scaleC = 1.3 + (Math.cos(degToRad(rot - 90)) / 6);
let offsetX = Math.cos(degToRad(rot + 90)) * 50 + (1 - scaleA);
let offsetY = Math.sin(degToRad(rot + 90)) * 50 + (1 - scaleB);
if (theme.glow) {
ctx.globalCompositeOperation = 'lighter';
drawGlow(-offsetX, -offsetY, scaleA, theme.glow_b);
drawGlow(0, 0, scaleC, theme.glow_a);
drawGlow(offsetX, offsetY, scaleB, theme.glow_c);
ctx.globalCompositeOperation = 'source-over';
}
if (theme.notches) {
for (let i = 0; i < 12 * 5; i++) {
if (i % 15 === 0)
drawCircle(degToRad(i / (12 * 5) * 360 - 90), clockRadius - NOTCH_BUFFER - 4, 8, COLOR_MINUTE_HAND)
else if (i % 5 === 0 && theme.notches >= 2)
drawCircle(degToRad(i / (12 * 5) * 360 - 90), clockRadius - NOTCH_BUFFER - 4, 6, COLOR_LIGHT_OVERLAY)
else if (theme.notches >= 3)
drawCircle(degToRad(i / (12 * 5) * 360 - 90), clockRadius - NOTCH_BUFFER - 4, 4, COLOR_LIGHT_DIM)
}
}
}
if (theme.events) {
for (let event of events) {
drawEvent(event);
}
}
drawCircle(0, 0, 36, COLOR_LIGHT_OVERLAY);
drawCircle(0, 0, 20, '#fff');
if (theme.minutes) {
const minuteAngle = degToRad((time.getMinutes() + time.getSeconds() / 60) / 60 * 360 - 180);
drawRoundedRect(34, clockRadius - 64, minuteAngle, 16, COLOR_MINUTE_HAND, undefined, undefined);
}
const hourAngle = degToRad(((time.getHours() % 12) + time.getMinutes() / 60) / 12 * 360 - 180);
drawRoundedRect(36, clockRadius - 80, hourAngle, 16, 'rgba(0, 0, 0, 0.15)', '#fff', 4);
if (!ambient) setTimeout(() => window.requestAnimationFrame(drawWatchFace), 1000/10);
}
window.requestAnimationFrame(drawWatchFace);
window.addEventListener('timetick', drawWatchFace);
window.addEventListener('ambientmodechanged', (e: any) => {
ambient = e.detail.ambientMode;
drawWatchFace();
});
document.addEventListener('visibilitychange', () => {
if (!document.hidden) drawWatchFace();
});
}
new Watch(artist);
})();

41
watch/src/Theme.ts Normal file
View File

@ -0,0 +1,41 @@
/**
* Specifies how the notches should be rendered.
*/
export const NotchMode = {
NONE: 0,
QUARTER: 1,
HOURS: 2,
MINUTES: 3
};
/**
* Theme preferences that determine how the watch should be rendered.
*/
export interface Theme {
notchMode: number;
showEvents: boolean;
showMinutes: boolean;
showGlow: boolean;
glowColors: [ string, string, string ];
}
/**
* Returns the theme preferences set by the user.
*/
export function getTheme(): Theme {
return {
notchMode: NotchMode.MINUTES,
showEvents: true,
showMinutes: true,
showGlow: true,
glowColors: [
'#4294ff',
'rgba(5, 124, 242, 0.6)',
'rgba(198, 0, 237, 1)'
]
};
}

11
watch/src/Util.ts Normal file
View File

@ -0,0 +1,11 @@
/**
* Converts the degree angle provided to radians.
*
* @param deg - The angle in degrees.
* @returns - The same angle in radians.
*/
export function degToRad(deg: number) {
return deg * (Math.PI / 180);
}

148
watch/src/Watch.ts Normal file
View File

@ -0,0 +1,148 @@
import Artist from './Artist';
import { degToRad } from './Util';
import { getEvents, Event } from './Events';
import { getTheme, NotchMode, Theme } from './Theme';
// TODO: placeholders
const COLOR_LIGHT_DIM = 'rgba(65, 199, 232, 0.15)';
const COLOR_LIGHT_OVERLAY = 'rgba(65, 199, 232, 0.33)';
const COLOR_MINUTE_HAND = 'rgba(65, 199, 232, 0.8)';
/** The minimum distance that all elements should be from the screen edge. */
const OUTER_BUFFER = 2;
/** The distance that elements should be away from the screen edge in addition to OUTER_BUFFER if notches are drawn. */
const NOTCH_BUFFER = 12;
/** The text size of the date. */
const DATE_SIZE = 20;
/**
* Handles app events, drawing using the artist, ambient mode, and watch <-> companion communication.
*/
export default class Watch {
private theme: Theme;
private events: Event[];
private ambient: boolean;
private animStep: number;
private animReq?: number;
private animTimeout?: number;
constructor(private artist: Artist) {
this.animStep = 0;
this.ambient = false;
this.theme = getTheme();
this.events = getEvents();
/** Triggered only in ambient mode. */
window.addEventListener('timetick', () => this.draw());
/** Update variables and redraw when the ambient mode changes. */
window.addEventListener('ambientmodechanged', (e: any) => {
const ambient = e.detail.ambientMode;
if (this.ambient === ambient) return;
this.ambient = ambient;
this.draw();
});
/** Immediately rerender when the visibility state chanegs. */
document.addEventListener('visibilitychange', () => {
if (!document.hidden) this.draw();
});
this.draw();
}
/**
* Clears pending draws.
*/
private clearPendingDraw() {
if (this.animTimeout !== undefined) window.clearTimeout(this.animTimeout);
this.animTimeout = undefined;
if (this.animReq !== undefined) window.cancelAnimationFrame(this.animReq);
this.animReq = undefined;
}
private getTime() {
try { return tizen.time.getCurrentDateTime(); }
catch (e) { return new Date(); }
}
private draw() {
const { artist, theme, events } = this;
this.clearPendingDraw();
artist.clear();
const time = this.getTime();
artist.ctx.font = `900 ${DATE_SIZE}px Arial sans-serif`;
artist.ctx.fillStyle = '#aaa';
artist.ctx.textBaseline = 'bottom';
artist.ctx.textAlign = 'center';
artist.ctx.fillText(`${['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'][time.getDay()]} ${time.getDate()}`,
artist.radius, artist.radius + artist.radius / 1.75);
artist.ctx.fillStyle = '#666';
artist.ctx.fillRect(artist.radius - 1 * DATE_SIZE,
artist.radius + artist.radius / 1.75 + 2, 2 * DATE_SIZE, 2);
if (!this.ambient && theme.showGlow) {
this.animStep = (this.animStep + 1) % 360;
let scaleA = 0.8 + (Math.sin(degToRad(this.animStep + 180)) / 6);
let scaleB = 0.8 + (Math.cos(degToRad(this.animStep)) / 6);
let scaleC = 1.2 + (Math.sin(degToRad((this.animStep * 3) + 90)) / 6);
let offset = Math.cos(degToRad(this.animStep * 3)) * 50;
artist.ctx.globalCompositeOperation = 'lighter';
artist.glow(degToRad(this.animStep), offset, scaleA, theme.glowColors[1]);
artist.glow(0, 0, scaleC, theme.glowColors[0]);
artist.glow(degToRad(this.animStep + 180), offset, scaleB, theme.glowColors[2]);
artist.ctx.globalCompositeOperation = 'source-over';
}
if (theme.notchMode !== NotchMode.NONE) {
for (let i = 0; i < 12 * 5; i++) {
if (i % 15 === 0) {
artist.circle(degToRad(i / (12 * 5) * 360),
artist.radius - OUTER_BUFFER - 4, 4, COLOR_MINUTE_HAND);
}
else if (i % 5 === 0 && theme.notchMode >= NotchMode.HOURS) {
artist.circle(degToRad(i / (12 * 5) * 360 - 90),
artist.radius - OUTER_BUFFER - 4, 3, COLOR_LIGHT_OVERLAY);
}
else if (theme.notchMode >= NotchMode.MINUTES) {
artist.circle(degToRad(i / (12 * 5) * 360 - 90),
artist.radius - OUTER_BUFFER - 4, 2, COLOR_LIGHT_DIM);
}
}
}
if (theme.showEvents) {
for (let event of events) {
let eventBuffer = OUTER_BUFFER + theme.notchMode !== NotchMode.NONE ? NOTCH_BUFFER : 0;
artist.event(event, artist.radius - eventBuffer);
}
}
artist.circle(0, 0, 18, COLOR_LIGHT_OVERLAY);
artist.circle(0, 0, 10, '#fff');
if (theme.showMinutes) {
const minuteAngle = degToRad((time.getMinutes() + time.getSeconds() / 60) / 60 * 360 - 180);
artist.rounded(minuteAngle, 40, artist.radius - 64, 16, COLOR_MINUTE_HAND);
}
const hourAngle = degToRad(((time.getHours() % 12) + time.getMinutes() / 60) / 12 * 360 - 180);
artist.rounded(hourAngle, 42, artist.radius - 80, 16, 'rgba(0, 0, 0, 0.15)', '#fff', 4);
if (!this.ambient) this.animTimeout = setTimeout(() =>
this.animReq = window.requestAnimationFrame(() => this.draw()),
1000/10) as any;
}
}

View File

@ -3,13 +3,16 @@
"strict": true,
"alwaysStrict": true,
"target": "es5",
"outFile": "build/main.js",
"target": "es6",
"module": "commonjs",
"esModuleInterop": true,
"moduleResolution": "node",
"lib": [ "dom", "es2015" ],
"typeRoots": [ "./node_modules/@types/" ],
"noEmitHelpers": true,
"importHelpers": true,
"removeComments": true,
"noUnusedLocals": true,
"noImplicitReturns": true,

1
watch/version.txt Normal file
View File

@ -0,0 +1 @@
1.0.5

53
watch/webpack.ts Normal file
View File

@ -0,0 +1,53 @@
import ForkTsCheckerPlugin from 'fork-ts-checker-webpack-plugin';
export default function() {
return {
mode: 'production',
stats: 'errors-warnings',
entry: { main: './src/Main.ts' },
resolve: {
extensions: [ '.ts', '.js' ]
},
output: {
path: __dirname + '/build',
filename: 'main.js'
},
plugins: [
new ForkTsCheckerPlugin({
typescript: {
configFile: './tsconfig.json',
},
eslint: {
files: './src/**/*.ts',
options: {
configFile: './.eslintrc.js',
emitErrors: true,
failOnHint: true,
typeCheck: true
}
}
})
],
module: {
rules: [{
test: /\.[t|j]s$/,
loader: 'babel-loader',
options: {
babelrc: false,
cacheDirectory: true,
presets: [
[ '@babel/preset-typescript' ],
[ '@babel/preset-env', { targets: { browsers: [ 'Chrome 90' ]} }]
],
plugins: [
// ['@babel/transform-react-jsx', { pragma: 'h' }],
// ['@babel/plugin-proposal-class-properties', { loose: true }],
// ['@babel/plugin-proposal-private-methods', { loose: true }]
]
}
}]
}
}
}