kenat
Version:
A JavaScript library for the Ethiopian calendar with date and time support.
274 lines (243 loc) • 9.47 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ethiopian Time Clock</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-align: center;
padding: 2rem;
background: #f9f9f9;
}
#digitalClock {
font-size: 2.5rem;
margin-bottom: 1rem;
}
#geezToggle {
margin-bottom: 2rem;
padding: 0.5rem 1rem;
font-size: 1rem;
cursor: pointer;
}
#analogClock {
margin: 0 auto;
background: #fff;
border: 5px solid #333;
border-radius: 50%;
width: 250px;
height: 250px;
position: relative;
}
#analogClock canvas {
display: block;
margin: 0 auto;
background: transparent;
}
</style>
</head>
<body>
<h1>Ethiopian Time Clock</h1>
<button id="geezToggle">Use Geez Numerals: ON</button>
<div id="digitalClock">--:-- --</div>
<div id="analogClock">
<canvas id="clockCanvas" width="250" height="250"></canvas>
</div>
<script type="module">
// --- Start of Embedded Library Code ---
// Note: GeezConverterError is simplified for this standalone file.
class GeezConverterError extends Error {
constructor(message) {
super(message);
this.name = 'GeezConverterError';
}
}
const geezSymbols = {
ones: ['', '፩', '፪', '፫', '፬', '፭', '፮', '፯', '፰', '፱'],
tens: ['', '፲', '፳', '፴', '፵', '፶', '፷', '፸', '፹', '፺'],
hundred: '፻',
tenThousand: '፼'
};
/**
* Converts a natural number to Ethiopic numeral string.
*/
function toGeez(input) {
if (typeof input !== 'number' && typeof input !== 'string') {
throw new GeezConverterError("Input must be a number or a string.");
}
const num = Number(input);
if (isNaN(num) || !Number.isInteger(num) || num < 0) {
throw new GeezConverterError("Input must be a non-negative integer.");
}
if (num === 0) return '0';
function convertBelow100(n) {
if (n <= 0) return '';
const tensDigit = Math.floor(n / 10);
const onesDigit = n % 10;
return geezSymbols.tens[tensDigit] + geezSymbols.ones[onesDigit];
}
if (num < 100) {
return convertBelow100(num);
}
if (num === 100) return geezSymbols.hundred;
if (num < 10000) {
const hundreds = Math.floor(num / 100);
const remainder = num % 100;
const hundredPart = (hundreds > 1 ? convertBelow100(hundreds) : '') + geezSymbols.hundred;
return hundredPart + convertBelow100(remainder);
}
const tenThousandPart = Math.floor(num / 10000);
const remainder = num % 10000;
const tenThousandGeez = (tenThousandPart > 1 ? toGeez(tenThousandPart) : '') + geezSymbols.tenThousand;
return tenThousandGeez + (remainder > 0 ? toGeez(remainder) : '');
}
// Note: InvalidTimeError is simplified for this standalone file.
class InvalidTimeError extends Error {
constructor(message) {
super(message);
this.name = 'InvalidTimeError';
}
}
class Time {
constructor(hour, minute = 0, period = 'day') {
if (hour < 1 || hour > 12) {
throw new InvalidTimeError(`Invalid Ethiopian hour: ${hour}. Must be between 1 and 12.`);
}
if (minute < 0 || minute > 59) {
throw new InvalidTimeError(`Invalid minute: ${minute}. Must be between 0 and 59.`);
}
if (period !== 'day' && period !== 'night') {
throw new InvalidTimeError(`Invalid period: "${period}". Must be 'day' or 'night'.`);
}
this.hour = hour;
this.minute = minute;
this.period = period;
}
static fromGregorian(hour, minute = 0) {
if (hour < 0 || hour > 23) {
throw new InvalidTimeError(`Invalid Gregorian hour: ${hour}. Must be between 0 and 23.`);
}
if (minute < 0 || minute > 59) {
throw new InvalidTimeError(`Invalid minute: ${minute}. Must be between 0 and 59.`);
}
let tempHour = hour - 6;
if (tempHour < 0) {
tempHour += 24;
}
const period = (tempHour < 12) ? 'day' : 'night';
let ethHour = tempHour % 12;
ethHour = (ethHour === 0) ? 12 : ethHour;
return new Time(ethHour, minute, period);
}
format(options = {}) {
const defaultLang = options.useGeez === false ? 'english' : 'amharic';
const { lang = defaultLang, useGeez = true, showPeriodLabel = true, zeroAsDash = true } = options;
const formatNum = (num) => {
if (useGeez) return toGeez(num);
return num.toString().padStart(2, '0');
};
const hourStr = formatNum(this.hour);
let minuteStr;
if (zeroAsDash && this.minute === 0) {
minuteStr = '_';
} else {
minuteStr = useGeez ? toGeez(this.minute) : this.minute.toString().padStart(2, '0');
}
let periodLabel = '';
if (showPeriodLabel) {
if (lang === 'english') {
periodLabel = this.period;
} else {
const amharicLabels = { day: 'ጠዋት', night: 'ማታ' };
periodLabel = amharicLabels[this.period];
}
}
const label = periodLabel ? ` ${periodLabel}` : '';
return `${hourStr}:${minuteStr}${label}`;
}
}
// --- End of Embedded Library Code ---
// --- Start of Clock Application Code ---
const digitalClock = document.getElementById('digitalClock');
const geezToggleBtn = document.getElementById('geezToggle');
const canvas = document.getElementById('clockCanvas');
const ctx = canvas.getContext('2d');
const radius = canvas.height / 2;
ctx.translate(radius, radius);
let useGeez = true;
geezToggleBtn.addEventListener('click', () => {
useGeez = !useGeez;
geezToggleBtn.textContent = `Use Geez Numerals: ${useGeez ? 'ON' : 'OFF'}`;
// No need to call drawClock immediately, the update loop will handle it.
});
function drawClock() {
drawFace(ctx, radius);
drawNumbers(ctx, radius);
drawTime(ctx, radius);
}
function drawFace(ctx, radius) {
ctx.beginPath();
ctx.arc(0, 0, radius, 0, 2 * Math.PI);
ctx.fillStyle = 'white';
ctx.fill();
ctx.strokeStyle = '#333';
ctx.lineWidth = radius * 0.05;
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, radius * 0.05, 0, 2 * Math.PI);
ctx.fillStyle = '#333';
ctx.fill();
}
function drawNumbers(ctx, radius) {
const angIncrement = (2 * Math.PI) / 12;
ctx.font = `${radius * 0.15}px Arial`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
for (let num = 1; num <= 12; num++) {
let numeral = useGeez ? toGeez(num) : num.toString();
// Position numbers on the clock face
let ang = num * angIncrement - (Math.PI / 2); // Adjust to start 1 at the top-right
let x = radius * 0.85 * Math.cos(ang);
let y = radius * 0.85 * Math.sin(ang);
ctx.fillStyle = '#000';
ctx.fillText(numeral, x, y);
}
}
function drawTime(ctx, radius) {
const now = new Date();
const time = Time.fromGregorian(now.getHours(), now.getMinutes());
const hour = time.hour;
const minute = time.minute;
const second = now.getSeconds();
// Hour hand angle calculation
const hourForAngle = hour % 12 + minute / 60; // Get a fractional hour (e.g., 1.5 for 1:30)
const hourAngle = (hourForAngle * Math.PI) / 6 - Math.PI / 2;
drawHand(ctx, hourAngle, radius * 0.5, radius * 0.07);
// Minute hand angle
const minuteAngle = (minute * Math.PI) / 30 - Math.PI / 2;
drawHand(ctx, minuteAngle, radius * 0.75, radius * 0.05);
// Second hand angle
const secondAngle = (second * Math.PI) / 30 - Math.PI / 2;
drawHand(ctx, secondAngle, radius * 0.85, radius * 0.02, 'red');
digitalClock.textContent = time.format({ useGeez, showPeriodLabel: true, zeroAsDash: false });
}
function drawHand(ctx, pos, length, width, color = '#333') {
ctx.beginPath();
ctx.lineWidth = width;
ctx.lineCap = 'round';
ctx.strokeStyle = color;
ctx.moveTo(0, 0);
ctx.lineTo(length * Math.cos(pos), length * Math.sin(pos));
ctx.stroke();
}
function update() {
ctx.clearRect(-radius, -radius, canvas.width, canvas.height);
drawClock();
requestAnimationFrame(update);
}
// Start the clock
update();
</script>
</body>
</html>