关于 Web components
科普非我所长,大家可以参考这里:Web Components | MDN (mozilla.org)
我的小尝试
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>Digi Clock</title>
<style>
html,
body {
height: 100%;
padding: 0;
margin: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
}
.bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background-image: url(R0000555.jpg);
background-size: cover;
opacity: .5;
}
</style>
</head>
<body>
<div class="bg"></div>
<digi-clock />
</body>
</html>
<script>
class DigiClock extends HTMLElement {
constructor() {
super();
this.shadowDOM = this.attachShadow({ mode: 'open' });
this.init = false;
this.initStyle();
this.setupState();
this.initVDOM();
requestAnimationFrame(() => this.render());
setInterval(() => { this.state.dt = new Date() }, 100);
}
getDateInfo(key, arr) {
return arr.filter((ele) => ele.type === key)[0].value;
}
updateText(val) {
const info = new Intl.DateTimeFormat("en", {
hour: "2-digit",
minute: "2-digit",
hour12: true,
weekday: "long",
day: "2-digit",
month: "long",
}).formatToParts(val);
this.state.hour = this.getDateInfo("hour", info);
this.state.min = this.getDateInfo("minute", info);
this.state.weekday = this.getDateInfo("weekday", info);
this.state.dayPeriod = this.getDateInfo("dayPeriod", info);
this.state.day = this.getDateInfo("day", info);
this.state.month = this.getDateInfo("month", info);
}
updateHourMinutePanel(val) {
const min = val.getMinutes();
let hour = val.getHours();
const sec = val.getSeconds();
if (hour >= 12) {
hour -= 12;
}
const hourDeg = hour * 30 (min / 60) * 30;
this.state.hourStyle = {
transform: `rotate(${hourDeg}deg)`,
};
const minDeg = min * 6 (sec / 60) * 6;
this.state.minStyle = {
transform: `rotate(${minDeg}deg)`,
};
};
updateSecondPanel(val) {
const min = val.getMinutes();
const sec = val.getSeconds();
const temp = Array(60).fill(false);
for (const secKey in this.state.secondCtrl) {
if (min % 2) {
temp[secKey] = secKey <= sec;
} else {
temp[secKey] = secKey > sec;
}
}
this.state.secondCtrl = temp;
};
setupState() {
this.dep = [];
this.depResult = {};
this.waitForRender = [];
this.watcher = {
dt: () => {
this.updateSecondPanel(this.state.dt);
this.updateHourMinutePanel(this.state.dt);
this.updateText(this.state.dt);
}
}
this.state = new Proxy({
dt: null,
secondCtrl: Array(60).fill(false),
hour: '',
min: '',
dayPeriod: '',
hourStyle: {},
minStyle: {},
weekday: '',
day: '',
month: ''
}, {
set: (state, prop, value) => {
if (state[prop] === value) {
return true;
}
state[prop] = value;
this.watcher[prop] && this.watcher[prop]();
this.dep.forEach(ele => {
if (ele.depList.includes(prop)) {
this.depResult[ele.id] = ele.func(state);
this.waitForRender.push(ele.id);
}
});
return true;
}
});
}
initStyle() {
const style = document.createElement('style');
style.textContent = `
.digi-clock {
width: 400px;
height: 400px;
position: relative;
}
.digi-clock > .second-panel {
width: 380px;
height: 380px;
top: 10px;
left: 10px;
position: absolute;
z-index: 0;
}
.digi-clock > .second-panel > .second-box {
width: 10px;
height: 30px;
border: 1px solid rgba(85, 102, 119, 1);
position: absolute;
top: 175px;
left: 185px;
transition: background 0.3s linear;
background: rgba(85, 102, 119, 0);
}
.digi-clock > .second-panel > .second-box.solid {
background-color: rgba(85, 102, 119, 1);
}
.digi-clock > .second-panel > .second-box.sp {
background-color: rgba(17, 34, 51, 0);
border: 2px solid rgba(17, 34, 51, 1);
}
.digi-clock > .second-panel > .second-box.sp.solid {
background-color: rgba(17, 34, 51, 1) !important;
}
.digi-clock > .hour-minute-panel {
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
}
.digi-clock > .hour-minute-panel > .hour,
.digi-clock > .hour-minute-panel > .minute {
transform-origin: bottom;
position: absolute;
}
.digi-clock > .hour-minute-panel > .hour {
width: 4px;
height: 120px;
background-color: #781138;
top: 80px;
left: 198px;
box-shadow: 2px -2px 3px 2px #555;
}
.digi-clock > .hour-minute-panel > .minute {
width: 2px;
height: 150px;
background-color: #035373;
top: 50px;
left: 199px;
box-shadow: 3px -2px 3px 2px #555;
}
.digi-clock > .hour-minute-panel > .dot {
position: absolute;
width: 20px;
height: 20px;
background: #000;
border-radius: 10px;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
.digi-clock > .date-text {
position: absolute;
width: 100%;
height: 100%;
z-index: 2;
font-family: "Oxanium";
}
.digi-clock > .date-text > .dt {
font-size: 2.5em;
height: 38px;
width: 103px;
line-height: 45px;
text-align: center;
position: absolute;
left: 0px;
bottom: 0px;
}
.digi-clock > .date-text > .dp {
font-size: 1.4em;
position: absolute;
bottom: 0px;
left: 105px;
line-height: 0.9em;
}
.digi-clock > .date-text > .dd {
position: absolute;
bottom: 0px;
right: 0px;
font-size: 1.3em;
}
.digi-clock > .date-text > .dm {
position: absolute;
right: 0;
bottom: 30px;
font-size: 1.6em;
}
`;
this.shadowDOM.appendChild(style);
}
createDep(func, depList) {
const s = this.dep.length;
const id = Symbol(`dep_${s}`);
this.dep.push({
func, depList, id
});
this.depResult[id] = func(this.state);
return id;
}
initVDOM() {
const radius = 175;
const secondBoxStyle = Array(60)
.fill()
.map((...args) => {
const s = args[1];
const ang = s * 6;
const xArc = this.angleToArc(ang);
const yArc = this.angleToArc(90 - ang);
let x = radius * Math.sin(xArc);
let y = radius * Math.sin(yArc);
return {
transform: `rotate(${ang}deg) translateZ(0)`,
top: (y - radius) * -1 "px",
left: x radius 10 "px",
};
});
this.vDOM = {
className: ['digi-clock'],
children: [
{
className: ['second-panel'],
children: Array(60).fill().map((ele, index) => {
const obj = {
style: secondBoxStyle[index],
className: [
'second-box',
this.createDep(state => state.secondCtrl[index] ? 'solid' : '', ['secondCtrl'])
]
};
if ([0, 15, 30, 45].includes(index)) {
obj.className.push('sp');
}
return obj;
})
},
{
className: ['hour-minute-panel'], children: [
{
className: ['hour'],
style: this.createDep(state => state.hourStyle, ['hourStyle'])
},
{
className: ['minute'],
style: this.createDep(state => state.minStyle, ['minStyle'])
},
{ className: ['dot'] }
]
},
{
className: ['date-text'], children: [
{
className: ['dt'],
content: this.createDep(
({ hour, min }) => `${hour}:${min}`,
['hour', 'min']
)
},
{
className: ['dp'],
content: this.createDep(state => state.dayPeriod, ['dayPeriod'])
},
{
className: ['dd'],
content: this.createDep(
({ day, weekday }) => `${day} ${weekday}`,
['day', 'weekday']
)
},
{
className: ['dm'],
content: this.createDep(state => state.month, ['month'])
},
]
}
]
}
this.domMaping = {}
}
angleToArc(angle) { return (angle * Math.PI) / 180; }
renderElement(config, target) {
const element = target || document.createElement('div');
if (config.className) {
const classList = config.className.map(ele => {
if (typeof ele === 'symbol') {
if (!target) this.domMaping[ele] = { element, config };
return this.depResult[ele];
}
return ele;
}).join(' ');
element.className = classList;
}
if (config.style) {
let target;
if (typeof config.style === 'symbol') {
if (!target) this.domMaping[config.style] = { element, config };
target = this.depResult[config.style];
} else {
target = config.style;
}
Object.keys(target).forEach(keyName => {
element.style[keyName] = target[keyName];
})
}
if (config.content) {
if (typeof config.content === 'symbol') {
if (!target) this.domMaping[config.content] = { element, config };
element.innerHTML = this.depResult[config.content];
} else {
element.innerHTML = config.content;
}
}
if (!target && config.children) {
config.children.forEach(ele => {
element.appendChild(this.renderElement(ele));
});
}
return element
}
render() {
if (!this.init) {
const element = this.renderElement(this.vDOM);
this.shadowDOM.appendChild(element);
this.init = true;
requestAnimationFrame(() => this.render());
return;
}
if (this.waitForRender.length === 0) {
requestAnimationFrame(() => this.render());
return;
}
this.waitForRender.forEach(ele => {
this.renderElement(this.domMaping[ele].config, this.domMaping[ele].element);
})
this.waitForRender.length = 0;
requestAnimationFrame(() => this.render());
}
}
customElements.define('digi-clock', DigiClock);
</script>