552 lines
No EOL
15 KiB
Text
552 lines
No EOL
15 KiB
Text
---
|
|
title: Clock
|
|
tags:
|
|
- demoExample
|
|
---
|
|
{% assign bodyClass = 'body_clock' -%}
|
|
{% layout 'hippie/simple.liquid' %}
|
|
|
|
{% block body %}
|
|
<header class="io pos_fix pin_top pin_right pin_left">
|
|
<button id="tglFormat" title="Toggle hour display">12-Stunden</button>
|
|
<button id="tglSections" title="Toggle sections">Abschnitte</button>
|
|
</header>
|
|
<main>
|
|
<div class="wrap">
|
|
<canvas id="background" width="768" height="768"></canvas>
|
|
<canvas id="rings" width="768" height="768"></canvas>
|
|
</div>
|
|
</main>
|
|
{% endblock %}
|
|
|
|
{% block script %}
|
|
<script>
|
|
const canvasBkg = document.getElementById('background');
|
|
const ctxBkg = canvasBkg.getContext('2d');
|
|
const canvasRings = document.getElementById('rings');
|
|
const ctxRings = canvasRings.getContext('2d');
|
|
const wrap = document.querySelector('.wrap');
|
|
|
|
let is24HourFormat = true;
|
|
let drawSections = false;
|
|
let currentDate = new Date();
|
|
let currentMonth = currentDate.getMonth() + 1; // Get current month (0-11)
|
|
let daysInCurrentMonth = daysInMonth(currentMonth, currentDate.getFullYear());
|
|
|
|
const outerMargin = 8;
|
|
const ringWidth = 16;
|
|
const ringGap = 8;
|
|
let centerX = canvasRings.width / 2;
|
|
let centerY = canvasRings.height / 2;
|
|
let maxSize = (canvasRings.width / 2 - ringWidth / 2) - outerMargin;
|
|
const rings = [
|
|
{
|
|
radius: maxSize,
|
|
color: 'black'
|
|
},
|
|
{
|
|
radius: maxSize - (ringWidth + ringGap),
|
|
color: 'lightgrey'
|
|
},
|
|
{
|
|
radius: maxSize - (ringWidth + ringGap) * 2,
|
|
color: 'white'
|
|
},
|
|
{
|
|
radius: maxSize - (ringWidth + ringGap) * 3 - ringGap,
|
|
color: getComputedStyle(document.documentElement).getPropertyValue('--clock-color-yellow').trim()
|
|
},
|
|
{
|
|
radius: maxSize - (ringWidth + ringGap) * 4 - ringGap * 2,
|
|
color: getComputedStyle(document.documentElement).getPropertyValue('--clock-color-orange').trim()
|
|
},
|
|
{
|
|
radius: maxSize - (ringWidth + ringGap) * 5 - ringGap * 3,
|
|
color: getComputedStyle(document.documentElement).getPropertyValue('--clock-color-pink').trim()
|
|
},
|
|
{
|
|
radius: maxSize - (ringWidth + ringGap) * 6 - ringGap * 3,
|
|
color: getComputedStyle(document.documentElement).getPropertyValue('--clock-color-purple').trim()
|
|
}
|
|
];
|
|
const groups = [
|
|
{
|
|
radius: rings[1].radius,
|
|
color: `rgba(0, 0, 0, .05)`,
|
|
width: 72
|
|
},
|
|
{
|
|
radius: rings[3].radius,
|
|
color: `rgba(0, 0, 0, .1)`,
|
|
width: ringWidth + ringGap
|
|
},
|
|
{
|
|
radius: rings[4].radius,
|
|
color: `rgba(0, 0, 0, .15)`,
|
|
width: ringWidth + ringGap
|
|
},
|
|
{
|
|
radius: maxSize - (ringWidth + ringGap) * 6 - ringGap - ringGap / 2,
|
|
color: `rgba(0, 0, 0, .2)`,
|
|
width: ringWidth * 2 + ringGap * 2
|
|
}
|
|
];
|
|
const sectionMarkerColor = `rgba(0, 0, 0, .2)`;
|
|
let sections = [
|
|
{
|
|
amount: 60,
|
|
radius: maxSize,
|
|
width: 1
|
|
},
|
|
{
|
|
amount: 60,
|
|
radius: rings[1].radius,
|
|
width: 1
|
|
},
|
|
{
|
|
amount: is24HourFormat ? 24 : 12,
|
|
radius: rings[2].radius,
|
|
width: 3
|
|
},
|
|
{
|
|
amount: 7,
|
|
radius: rings[3].radius,
|
|
width: 5
|
|
},
|
|
{
|
|
amount: daysInCurrentMonth,
|
|
radius: rings[4].radius,
|
|
width: 3
|
|
},
|
|
{
|
|
amount: 365,
|
|
radius: rings[5].radius,
|
|
width: 1
|
|
},
|
|
{
|
|
amount: 12,
|
|
radius: rings[6].radius,
|
|
width: 3
|
|
}
|
|
];
|
|
|
|
document.getElementById('tglFormat').addEventListener('click', () => {
|
|
is24HourFormat = !is24HourFormat;
|
|
document.getElementById('tglFormat').textContent = is24HourFormat ? '12-Stunden-Format' : '24-Stunden-Format';
|
|
sections[2].amount = is24HourFormat ? 24 : 12;
|
|
drawBackground();
|
|
});
|
|
|
|
document.getElementById('tglSections').addEventListener('click', () => {
|
|
drawSections = !drawSections;
|
|
drawBackground();
|
|
});
|
|
|
|
// FIXME: Konstanten mit maxSize werden nicht verändert
|
|
window.addEventListener('resize', resizeClock);
|
|
|
|
resizeClock();
|
|
// TODO: TimeDisplay nutzen, ev. erweitern oder ähnliche Umsetzung anwenden
|
|
setInterval(updateRings, 1000);
|
|
|
|
function updateRings() {
|
|
currentDate = new Date();
|
|
const currentSeconds = currentDate.getSeconds();
|
|
const currentMinutes = currentDate.getMinutes();
|
|
const currentHours = currentDate.getHours();
|
|
const currentDayOfWeek = getNumericWeekday(currentDate);
|
|
const currentDayOfMonth = currentDate.getDate();
|
|
const currentDayOfYear = getNumericYearDay(currentDate);
|
|
currentMonth = currentDate.getMonth() + 1; // Get current month (0-11)
|
|
daysInCurrentMonth = daysInMonth(currentMonth, currentDate.getFullYear());
|
|
|
|
drawRings(
|
|
currentSeconds,
|
|
currentMinutes,
|
|
currentHours,
|
|
currentDayOfWeek,
|
|
currentDayOfMonth,
|
|
currentMonth,
|
|
currentDayOfYear,
|
|
daysInCurrentMonth
|
|
);
|
|
}
|
|
|
|
// TODO: Parameter für Wochenstart ergänzen
|
|
function getNumericWeekday(date) {
|
|
const weekday = date.getDay(); // 0 (Sunday) to 6 (Saturday)
|
|
return (weekday === 0) ? 7 : weekday;
|
|
}
|
|
|
|
function getNumericYearDay(date) {
|
|
const start = new Date(date.getFullYear(), 0, 0);
|
|
return Math.floor((date - start) / 86400000);
|
|
}
|
|
|
|
function daysInMonth(month, year) {
|
|
return new Date(year, month, 0).getDate();
|
|
}
|
|
|
|
function drawArc(value, maxValue, radius, strokeStyle) {
|
|
const startAngle = -0.5 * Math.PI; // Start at the top
|
|
const endAngle = startAngle + (2 * Math.PI * (value / maxValue));
|
|
|
|
ctxRings.lineWidth = 16;
|
|
ctxRings.strokeStyle = strokeStyle;
|
|
ctxRings.beginPath();
|
|
ctxRings.arc(centerX, centerY, radius, startAngle, endAngle, false);
|
|
ctxRings.stroke();
|
|
}
|
|
|
|
// TODO: Array rings nutzen
|
|
function drawRings(seconds, minutes, hours, dayOfWeek, dayOfMonth, month, dayOfYear, daysInCurrentMonth) {
|
|
ctxRings.clearRect(0, 0, canvasRings.width, canvasRings.height);
|
|
|
|
drawArc(
|
|
(seconds === 0) ? 60 : seconds,
|
|
60,
|
|
rings[0].radius,
|
|
rings[0].color
|
|
);
|
|
drawArc(
|
|
(minutes === 0) ? 60 : minutes,
|
|
60,
|
|
rings[1].radius,
|
|
rings[1].color
|
|
);
|
|
drawArc(
|
|
// is24HourFormat ? ((hours === 0) ? 24 : hours) : hours % 12,
|
|
is24HourFormat ? hours : hours % 12,
|
|
is24HourFormat ? 24 : 12,
|
|
rings[2].radius,
|
|
rings[2].color
|
|
);
|
|
drawArc(dayOfWeek, 7, rings[3].radius, rings[3].color);
|
|
drawArc(dayOfMonth, daysInCurrentMonth, rings[4].radius, rings[4].color);
|
|
drawArc(dayOfYear, 365, rings[5].radius, rings[5].color);
|
|
drawArc(month, 12, rings[6].radius, rings[6].color);
|
|
}
|
|
|
|
function drawBackground() {
|
|
ctxBkg.clearRect(0, 0, canvasBkg.width, canvasBkg.height);
|
|
|
|
groups.forEach((section) => {
|
|
ctxBkg.strokeStyle = section.color;
|
|
ctxBkg.lineWidth = section.width;
|
|
ctxBkg.beginPath();
|
|
ctxBkg.arc(centerX, centerY, section.radius, 0, 2 * Math.PI);
|
|
ctxBkg.stroke();
|
|
});
|
|
|
|
if (drawSections) {
|
|
sections.forEach((marker) => {
|
|
for (let i = 0; i < marker.amount; i++) {
|
|
const angle = (i * (360 / marker.amount) - 90) * (Math.PI / 180); // -90 to start at top
|
|
const outerRadius = marker.radius + ringWidth / 2;
|
|
const outerX = centerX + outerRadius * Math.cos(angle);
|
|
const outerY = centerY + outerRadius * Math.sin(angle);
|
|
const innerRadius = marker.radius - ringWidth / 2;
|
|
const innerX = centerX + innerRadius * Math.cos(angle);
|
|
const innerY = centerY + innerRadius * Math.sin(angle);
|
|
|
|
ctxBkg.strokeStyle = sectionMarkerColor;
|
|
ctxBkg.lineWidth = marker.width;
|
|
ctxBkg.beginPath();
|
|
ctxBkg.moveTo(outerX, outerY);
|
|
ctxBkg.lineTo(innerX, innerY);
|
|
ctxBkg.stroke();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function resizeClock() {
|
|
const height = window.innerHeight;
|
|
const width = window.innerWidth;
|
|
const windowDimension = {
|
|
value: Math.min(height, width),
|
|
smaller: height < width ? 'height' : 'width'
|
|
};
|
|
const clockSize = Math.floor(windowDimension.value * 0.9);
|
|
|
|
canvasBkg.style.width = clockSize + 'px';
|
|
canvasBkg.style.height = clockSize + 'px';
|
|
canvasRings.style.width = clockSize + 'px';
|
|
canvasRings.style.height = clockSize + 'px';
|
|
canvasBkg.width = clockSize;
|
|
canvasBkg.height = clockSize;
|
|
canvasRings.width = clockSize;
|
|
canvasRings.height = clockSize;
|
|
|
|
centerX = canvasRings.width / 2;
|
|
centerY = canvasRings.height / 2;
|
|
maxSize = (canvasRings.width / 2 - ringWidth / 2) - outerMargin;
|
|
|
|
wrap.style.width = clockSize + 'px';
|
|
wrap.style.height = clockSize + 'px';
|
|
|
|
drawBackground();
|
|
updateRings();
|
|
}
|
|
|
|
class HippieClock {
|
|
constructor(element, date, options) {
|
|
this.element = element;
|
|
this.date = this.getTime(date);
|
|
this.options = options || {
|
|
size: Math.floor(this.getSize().value * 0.9),
|
|
h24: true,
|
|
sections: true
|
|
};
|
|
this.parts = this.createContext(['bkg', 'hands']);
|
|
this.shapes = [];
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.addRing('seconds', 1, 21, 60, `rgb(250, 216, 3)`);
|
|
this.addRing('minutes', .9, 46, 60, `rgb(242, 175, 19)`);
|
|
this.addRing('minutes', .8, 6, this.options.h24 ? 24 : 12, `rgb(211, 10, 81)`);
|
|
this.addRing('dotweek', .7, 32, 7, `rgb(142, 31, 104)`);
|
|
this.addRing('dotmonth', .6, 12, this.getTime().daysMonth, `rgb(39, 63, 139)`);
|
|
this.addRing('dotyear', .5, 256, 365, `rgb(60, 87, 154)`);
|
|
this.addRing('month', .4, 10, 12, `rgb(183, 224, 240)`);
|
|
|
|
this.resize();
|
|
window.addEventListener('resize', () => this.resize());
|
|
|
|
console.debug(this);
|
|
}
|
|
|
|
resize() {
|
|
this.parts.forEach(part => {
|
|
part.element.width = part.element.offsetWidth;
|
|
part.element.height = part.element.offsetHeight;
|
|
this.draw();
|
|
});
|
|
}
|
|
|
|
draw() {
|
|
// TODO: Nur geänderte Teile löschen
|
|
this.parts.forEach(part => {
|
|
part.context.clearRect(0, 0, part.element.width, part.element.height);
|
|
});
|
|
|
|
let ctx = undefined;
|
|
|
|
this.shapes
|
|
.filter(item => item.type === 'circle')
|
|
.forEach((shape) => {
|
|
const radius = this.toPixelSize(shape.radius) / 2;
|
|
ctx = this.parts[0].context;
|
|
|
|
ctx.fillStyle = shape.color;
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
this.toPixelX(shape.center),
|
|
this.toPixelY(shape.center),
|
|
radius,
|
|
0,
|
|
Math.PI * 2
|
|
);
|
|
ctx.fill();
|
|
});
|
|
|
|
this.shapes
|
|
.filter(item => item.type === 'ring')
|
|
.forEach((shape) => {
|
|
if (this.options.sections) {
|
|
const outerRadius = this.toPixelSize(shape.radius) / 2;
|
|
const innerRadius = this.toPixelSize(shape.radius) / 2 - shape.stroke;
|
|
ctx = this.parts[0].context;
|
|
|
|
for (let i = 0; i < shape.max; i++) {
|
|
const angle = (i * (360 / shape.max) - 90) * (Math.PI / 180); // -90 to start at top
|
|
const outerX = this.toPixelX(shape.center) + outerRadius * Math.cos(angle);
|
|
const outerY = this.toPixelY(shape.center) + outerRadius * Math.sin(angle);
|
|
const innerX = this.toPixelX(shape.center) + innerRadius * Math.cos(angle);
|
|
const innerY = this.toPixelY(shape.center) + innerRadius * Math.sin(angle);
|
|
|
|
ctx.strokeStyle = 'black';
|
|
// TODO: Stärke an shape.max orientieren
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(outerX, outerY);
|
|
ctx.lineTo(innerX, innerY);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
const radius = this.toPixelSize(shape.radius) / 2 - shape.stroke / 2;
|
|
const start = -0.5 * Math.PI; // Start at the top
|
|
const angle = start + (2 * Math.PI * (shape.angle / shape.max));
|
|
ctx = this.parts[1].context;
|
|
|
|
ctx.strokeStyle = shape.color;
|
|
ctx.lineWidth = shape.stroke;
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
this.toPixelX(shape.center),
|
|
this.toPixelY(shape.center),
|
|
radius,
|
|
start,
|
|
angle
|
|
);
|
|
ctx.stroke();
|
|
});
|
|
}
|
|
|
|
update() {
|
|
const second = this.getTime().second;
|
|
const minute = this.getTime().minute;
|
|
const hour = this.getTime().hour;
|
|
|
|
this.updateShape('seconds', (second === 0) ? 60 : second);
|
|
this.updateShape('minutes', (minute === 0) ? 60 : minute);
|
|
this.updateShape('hours', this.options.h24 ? hour : hour % 12, this.options.h24 ? 24 : 12);
|
|
this.updateShape('dotweek', this.getTime().dayWeek);
|
|
this.updateShape('dotmonth', this.getTime().dayMonth, this.getTime().daysMonth);
|
|
this.updateShape('dotyear', this.getTime().dayYear);
|
|
this.updateShape('month', this.getTime().month);
|
|
this.draw();
|
|
}
|
|
|
|
toPixelX(number) {
|
|
return number * this.parts[0].element.width;
|
|
}
|
|
|
|
toPixelY(number) {
|
|
return number * this.parts[0].element.height;
|
|
}
|
|
|
|
toPixelSize(number) {
|
|
return number * Math.min(this.parts[0].element.width, this.parts[0].element.height);
|
|
}
|
|
|
|
// TODO: Parameter für Wochenstart ergänzen
|
|
getNumericWeekday(date) {
|
|
const weekday = date.getDay(); // 0 (Sunday) to 6 (Saturday)
|
|
|
|
return (weekday === 0) ? 7 : weekday;
|
|
}
|
|
|
|
getNumericYearDay(date) {
|
|
const start = new Date(date.getFullYear(), 0, 0);
|
|
|
|
return Math.floor((date - start) / 86400000);
|
|
}
|
|
|
|
daysInMonth(month, year) {
|
|
return new Date(year, month, 0).getDate();
|
|
}
|
|
|
|
getSize() {
|
|
const height = window.innerHeight;
|
|
const width = window.innerWidth;
|
|
|
|
return {
|
|
value: Math.min(height, width),
|
|
smaller: height < width ? 'height' : 'width'
|
|
};
|
|
}
|
|
|
|
getTime(date) {
|
|
this.date = date || new Date();
|
|
|
|
return {
|
|
second: this.date.getSeconds(),
|
|
minute: this.date.getMinutes(),
|
|
hour: this.date.getHours(),
|
|
dayWeek: this.getNumericWeekday(this.date),
|
|
dayMonth: this.date.getDate(),
|
|
dayYear: this.getNumericYearDay(this.date),
|
|
month: this.date.getMonth() + 1, // Get current month (0-11)
|
|
daysMonth: this.daysInMonth(this.date.getMonth() + 1, this.date.getFullYear())
|
|
};
|
|
}
|
|
|
|
createContext(names) {
|
|
let parts = [];
|
|
const wrap = document.createElement('div');
|
|
|
|
wrap.style.position = 'relative';
|
|
wrap.style.height = this.options.size + 'px';
|
|
wrap.style.width = this.options.size + 'px';
|
|
|
|
names.forEach(name => {
|
|
const canvas = document.createElement('canvas');
|
|
|
|
canvas.style.position = 'absolute';
|
|
canvas.style.top = '0px';
|
|
canvas.style.left = '0px';
|
|
canvas.style.height = '100%';
|
|
canvas.style.width = '100%';
|
|
canvas.height = canvas.offsetHeight;
|
|
canvas.width = canvas.offsetWidth;
|
|
|
|
wrap.appendChild(canvas);
|
|
|
|
parts.push({name: name, element: canvas, context: canvas.getContext('2d')});
|
|
});
|
|
|
|
this.element.appendChild(wrap);
|
|
|
|
return parts;
|
|
}
|
|
|
|
updateShape(id, angle, max) {
|
|
const shape = this.shapes.find(s => s.id === id);
|
|
|
|
if (shape) {
|
|
shape.angle = angle;
|
|
if (max) shape.max = max;
|
|
}
|
|
}
|
|
|
|
updateOptions(changes) {
|
|
this.options = {...this.options, ...changes};
|
|
}
|
|
|
|
addCircle(id, center, radius, color = `rgba(0, 0, 0, .1)`) {
|
|
this.shapes.push({type: 'circle', id, center, radius, color});
|
|
}
|
|
|
|
addRing(id, radius, angle, max, color = 'black', center = .5, stroke = 16) {
|
|
this.shapes.push({type: 'ring', id, radius, angle, max, color, center, stroke});
|
|
}
|
|
|
|
addSection(id, radius, color = 'black', center = .5, stroke = 1) {
|
|
this.shapes.push({type: 'section', id, radius, color, center, stroke});
|
|
}
|
|
}
|
|
|
|
const container = document.querySelector('#clock main');
|
|
const clock = new HippieClock(container);
|
|
|
|
clock.addCircle('base', .5, 1);
|
|
clock.draw();
|
|
|
|
setInterval(() => {
|
|
clock.update();
|
|
}, 1000);
|
|
|
|
// TODO: Aktionen gehören quasi zu HippieClock
|
|
document.getElementById('tglFormat').addEventListener('click', () => {
|
|
if (clock) {
|
|
clock.updateOptions({h24: !clock.options.h24})
|
|
document.getElementById('tglFormat').textContent = clock.options.h24 ? '12-Stunden' : '24-Stunden';
|
|
} else {
|
|
console.log('No clock available');
|
|
}
|
|
});
|
|
|
|
document.getElementById('tglSections').addEventListener('click', () => {
|
|
if (clock) {
|
|
clock.updateOptions({sections: !clock.options.sections})
|
|
} else {
|
|
console.log('No clock available');
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %} |