feat: Change file structure
- Move return object to named export for 11ty config - screens is now view - 11ty data files moved to view/_data - templates is now view/_includes - Both are the default directories - data is now used as intended, for user data - Update index to reflect filenames and structure
This commit is contained in:
parent
610e22b3c9
commit
b67a8a893a
78 changed files with 21 additions and 19 deletions
|
|
@ -1,488 +0,0 @@
|
|||
---
|
||||
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="tglOverlay" title="Toggle overlay">☰</button>
|
||||
<nav>
|
||||
<button id="tglFormat" title="Toggle hour display">12-Stunden</button>
|
||||
<button id="tglSections" title="Toggle sections">Abschnitte</button>
|
||||
</nav>
|
||||
</header>
|
||||
<main></main>
|
||||
{% endblock %}
|
||||
|
||||
{% block assets %}
|
||||
<script src="/vendor/hippie-script.js"></script>
|
||||
<script src="/js/globals.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
class HippieClock {
|
||||
constructor(element, date = new Date(), options = {}) {
|
||||
this.element = element;
|
||||
this.date = date;
|
||||
this.defaults = {
|
||||
debug: true,
|
||||
size: Math.floor(this.getSize().value * 0.9),
|
||||
h24: true,
|
||||
sections: true,
|
||||
overlay: false
|
||||
};
|
||||
this.options = {...this.defaults, ...options};
|
||||
this.values = this.getTime();
|
||||
this.parts = [];
|
||||
this.shapes = [];
|
||||
|
||||
this.#init();
|
||||
}
|
||||
|
||||
#init() {
|
||||
this.#createContext(['background', 'hands']);
|
||||
// this.createOverlay();
|
||||
|
||||
this.addRing('seconds', 1, 21, 60, `rgb(250, 216, 3)`);
|
||||
this.addRing('minutes', .9, 46, 60, `rgb(242, 175, 19)`);
|
||||
this.addRing('hours', .8, 6, this.options.h24 ? 24 : 12, `rgb(211, 10, 81)`);
|
||||
this.addRing('dotweek', .7, 2, 7, `rgb(142, 31, 104)`);
|
||||
this.addRing('dotmonth', .6, 12, this.values.daysMonth, `rgb(39, 63, 139)`);
|
||||
this.addRing('dotyear', .5, 256, this.values.daysYear, `rgb(60, 87, 154)`);
|
||||
this.addRing('week', .4, 10, this.values.weeksYear, `rgb(183, 224, 240)`);
|
||||
this.addRing('month', .3, 10, 12, `rgb(107, 199, 217)`);
|
||||
this.addRing('moon', .2, 4, 8, `rgb(82, 190, 209)`);
|
||||
|
||||
this.#resize();
|
||||
window.addEventListener('resize', () => this.#resize());
|
||||
|
||||
// console.debug(this);
|
||||
if (this.options.debug) {
|
||||
console.group('Clock');
|
||||
console.info('\nOptions:', this.options, '\n\n');
|
||||
console.info('Date:', this.date);
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
#resize() {
|
||||
this.updateOptions({size: Math.floor(this.getSize().value * 0.9)});
|
||||
this.parts.forEach(part => {
|
||||
if (part.name === 'wrap') {
|
||||
part.element.style.height = this.options.size + 'px';
|
||||
part.element.style.width = this.options.size + 'px';
|
||||
}
|
||||
|
||||
// part.element.width = part.element.offsetWidth;
|
||||
// part.element.height = part.element.offsetHeight;
|
||||
part.element.width = this.options.size;
|
||||
part.element.height = this.options.size;
|
||||
|
||||
this.draw();
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Zuweisung von shapes zu parts anpassen
|
||||
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 = `rgba(0, 0, 0, .25)`;
|
||||
ctx.lineWidth = mapRange(shape.max, 7, 72, .1, 5, true, true);
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Erfassung für geänderte Formen ergänzen
|
||||
update() {
|
||||
this.values = this.getTime();
|
||||
const second = this.values.second;
|
||||
const minute = this.values.minute;
|
||||
const hour = this.values.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.values.dayWeek);
|
||||
this.updateShape('dotmonth', this.values.dayMonth, this.values.daysMonth);
|
||||
this.updateShape('dotyear', this.values.dayYear);
|
||||
this.updateShape('week', this.values.week);
|
||||
this.updateShape('month', this.values.month);
|
||||
this.updateShape('moon', this.values.moon);
|
||||
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: DateDisplay und TimeDisplay benutzen
|
||||
// TODO: Parameter für Wochenstart ergänzen
|
||||
getWeekday(date) {
|
||||
const weekday = date.getDay(); // 0 (Sunday) to 6 (Saturday)
|
||||
|
||||
return (weekday === 0) ? 7 : weekday;
|
||||
}
|
||||
|
||||
getYearDay(date) {
|
||||
const start = new Date(date.getFullYear(), 0, 0);
|
||||
|
||||
return Math.floor((date - start) / 86400000);
|
||||
}
|
||||
|
||||
isLeapYear(year) {
|
||||
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
|
||||
}
|
||||
|
||||
getDaysInYear(year) {
|
||||
return this.isLeapYear(year) ? 366 : 365;
|
||||
}
|
||||
|
||||
getYearInfo(date) {
|
||||
const current = this.getYearDay(date);
|
||||
const total = this.getDaysInYear(date.getFullYear());
|
||||
const remaining = total - current;
|
||||
|
||||
return {
|
||||
total: total,
|
||||
current: current,
|
||||
remaining: remaining
|
||||
}
|
||||
}
|
||||
|
||||
getDaysInMonth(month, year) {
|
||||
return new Date(year, month, 0).getDate();
|
||||
}
|
||||
|
||||
// ISO 8601
|
||||
getISOWeek(date) {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const start = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
|
||||
return Math.ceil((((d - start) / 86400000) + 1) / 7);
|
||||
}
|
||||
|
||||
getISOWeeksInYear(year) {
|
||||
// Check if Dec 28 is in week 53
|
||||
return this.getISOWeek(new Date(year, 11, 28)) === 53 ? 53 : 52;
|
||||
}
|
||||
|
||||
getISOWeekInfo(date) {
|
||||
const current = this.getISOWeek(date);
|
||||
const weeksYear = this.getISOWeeksInYear(date.getFullYear());
|
||||
|
||||
return {
|
||||
current,
|
||||
weeksYear
|
||||
};
|
||||
}
|
||||
|
||||
getMoonNameFrom(phase) {
|
||||
if (phase < 0.125) return 'New Moon';
|
||||
if (phase < 0.25) return 'Waxing Crescent';
|
||||
if (phase < 0.375) return 'First Quarter';
|
||||
if (phase < 0.5) return 'Waxing Gibbous';
|
||||
if (phase < 0.625) return 'Full Moon';
|
||||
if (phase < 0.75) return 'Waning Gibbous';
|
||||
if (phase < 0.875) return 'Last Quarter';
|
||||
return 'Waning Crescent';
|
||||
}
|
||||
|
||||
getMoonPhase(date) {
|
||||
// Known new moon date: January 6, 2000
|
||||
const newMoon = new Date(2000, 0, 6);
|
||||
const lunarCycle = 29.53058867; // days
|
||||
|
||||
const daysSinceNewMoon = (date - newMoon) / (1000 * 60 * 60 * 24);
|
||||
const phase = (daysSinceNewMoon % lunarCycle) / lunarCycle;
|
||||
|
||||
return {
|
||||
illumination: Math.abs(Math.cos(Math.PI * phase)),
|
||||
phase: mapRange(phase, 0, 1, 1, 8),
|
||||
phaseName: this.getMoonNameFrom(phase)
|
||||
};
|
||||
}
|
||||
|
||||
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.getWeekday(this.date),
|
||||
dayMonth: this.date.getDate(),
|
||||
dayYear: this.getYearInfo(this.date).current,
|
||||
daysYear: this.getYearInfo(this.date).total,
|
||||
week: this.getISOWeekInfo(this.date).current,
|
||||
weeksYear: this.getISOWeekInfo(this.date).weeksYear,
|
||||
month: this.date.getMonth() + 1, // Get current month (0-11)
|
||||
daysMonth: this.getDaysInMonth(this.date.getMonth() + 1, this.date.getFullYear()),
|
||||
moon: this.getMoonPhase(this.date).phase
|
||||
};
|
||||
}
|
||||
|
||||
#createContext(names) {
|
||||
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);
|
||||
this.parts.push({name: name, element: canvas, context: canvas.getContext('2d')});
|
||||
});
|
||||
|
||||
this.element.appendChild(wrap);
|
||||
this.parts.push({name: 'wrap', element: wrap});
|
||||
}
|
||||
|
||||
createOverlay() {
|
||||
const overlay = document.createElement('div');
|
||||
const text = document.createElement('p');
|
||||
const timeElement = document.createElement('span');
|
||||
const dateElement = document.createElement('span');
|
||||
|
||||
new DateDisplay(dateElement);
|
||||
new TimeDisplay(timeElement);
|
||||
|
||||
Object.assign(overlay.style, {
|
||||
zIndex: 5,
|
||||
position: 'absolute',
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: this.options.overlay ? 'flex' : 'none',
|
||||
// display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: `rgba(0, 0, 0, .6)`,
|
||||
color: 'white'
|
||||
});
|
||||
overlay.dataset.part = 'overlay';
|
||||
|
||||
text.appendChild(timeElement);
|
||||
text.insertAdjacentText('beforeend', ' ');
|
||||
text.appendChild(dateElement);
|
||||
overlay.appendChild(text);
|
||||
this.element.appendChild(overlay);
|
||||
this.parts.push({name: 'overlay', element: overlay});
|
||||
}
|
||||
|
||||
removeOverlay() {
|
||||
const index = this.parts.findIndex(s => s.name === 'overlay');
|
||||
|
||||
if (index !== -1) {
|
||||
this.parts[index].element.remove();
|
||||
this.parts.splice(index, 1);
|
||||
this.draw();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Proxy für Optionen benutzen
|
||||
updateOptions(changes) {
|
||||
this.options = {...this.options, ...changes};
|
||||
|
||||
// TODO: Änderungen durch setzen von Optionen vornehmen, oder nicht?
|
||||
const drawOptions = new Set(['sections', 'h24']);
|
||||
const hasKey = Object.keys(changes).some(key => drawOptions.has(key));
|
||||
|
||||
if (hasKey) {
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
updateShape(id, angle, max) {
|
||||
const shape = this.shapes.find(s => s.id === id);
|
||||
|
||||
if (shape) {
|
||||
shape.angle = angle;
|
||||
if (max) shape.max = max;
|
||||
}
|
||||
}
|
||||
|
||||
removeShape(id) {
|
||||
const index = this.shapes.findIndex(s => s.id === id);
|
||||
|
||||
if (index !== -1) {
|
||||
this.shapes.splice(index, 1);
|
||||
this.draw();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
clearShapes() {
|
||||
this.shapes = [];
|
||||
this.draw();
|
||||
}
|
||||
|
||||
findShape(id) {
|
||||
return this.shapes.some(s => s.id === id);
|
||||
}
|
||||
|
||||
getShape(id) {
|
||||
return this.shapes.find(s => s.id === id);
|
||||
}
|
||||
|
||||
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 = 'white', 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();
|
||||
|
||||
// TODO: Alternative mit requestAnimationFrame()
|
||||
// TODO: Möglichkeit für Start/Stop wie bei TimeDisplay
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('tglOverlay').addEventListener('click', () => {
|
||||
if (clock) {
|
||||
// TODO: Anzeigen und ausblenden, anstatt löschen und hinzufügen?
|
||||
clock.updateOptions({overlay: !clock.options.overlay});
|
||||
|
||||
if (clock.options.overlay) {
|
||||
clock.createOverlay();
|
||||
} else {
|
||||
clock.removeOverlay();
|
||||
}
|
||||
} else {
|
||||
console.log('No clock available');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue