diff --git a/index.css b/index.css
index 8952c56..449707f 100644
--- a/index.css
+++ b/index.css
@@ -10,17 +10,195 @@ body {
-moz-user-select: moz-none;
-ms-user-select: none;
user-select: none;
+ overflow: hidden;
}
+
+#sky {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(180deg, #8fd3ff 0%, #cfefff 60%, #f7fbff 100%);
+ overflow: hidden;
+}
+
+#sun-wrap {
+ position: absolute;
+ left: 50%;
+ bottom: -60vh;
+ transform: translateX(-50%);
+ transition: bottom 0.4s linear;
+ pointer-events: none;
+ z-index: 9;
+}
+
+#sun {
+ position: absolute;
+ left: 50%;
+ bottom: 0;
+ width: var(--sun-size, 105px);
+ height: var(--sun-size, 105px);
+ margin-left: calc(var(--sun-size, 105px) / -2);
+ border-radius: 50%;
+ background: radial-gradient(circle at 30% 30%, #fffbe0, #ffd972 60%, #ffb52a 100%);
+ box-shadow: 0 0 70px rgba(255, 193, 55, 0.9), 0 0 140px rgba(255, 193, 55, 0.55);
+ transition: transform 0.4s ease-out, box-shadow 0.4s ease-out;
+ opacity: 0.85;
+}
+
+#sun-rays {
+ position: absolute;
+ left: 50%;
+ bottom: 0;
+ width: var(--sun-ray-size, 189px);
+ height: var(--sun-ray-size, 189px);
+ margin-left: calc(var(--sun-ray-size, 189px) / -2);
+ border-radius: 50%;
+ overflow: hidden;
+ filter: blur(1px);
+ opacity: 0.25;
+ transform: scale(0.9);
+ transition: opacity 0.4s ease-out, transform 0.4s ease-out;
+}
+
+#sun-rays::before {
+ content: "";
+ position: absolute;
+ top: -10%;
+ left: -10%;
+ width: 120%;
+ height: 120%;
+ border-radius: 50%;
+ background: repeating-conic-gradient(rgba(255, 225, 120, 0.55) 0deg, rgba(255, 225, 120, 0.55) 12deg, rgba(255, 225, 120, 0) 28deg, rgba(255, 225, 120, 0) 40deg);
+ animation: spin 16s linear infinite;
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+#rooster {
+ position: absolute;
+ left: 50%;
+ bottom: calc(var(--sun-size, 210px) + 10px);
+ transform: translateX(-50%);
+ font-size: 68px;
+ opacity: 0;
+ transition: opacity 0.25s ease-out;
+ filter: drop-shadow(0 6px 4px rgba(0, 0, 0, 0.15));
+ pointer-events: none;
+ z-index: 11;
+}
+
+#rooster.show {
+ opacity: 1;
+}
+
+#rooster.flap {
+ animation: flap 0.55s ease-in-out 3 forwards;
+}
+
+@keyframes flap {
+ 0% {
+ transform: translateX(-50%) translateY(0) rotate(0deg);
+ }
+ 50% {
+ transform: translateX(-50%) translateY(8px) rotate(-10deg);
+ }
+ 100% {
+ transform: translateX(-50%) translateY(0) rotate(10deg);
+ }
+}
+
+.cloud {
+ position: absolute;
+ top: 15%;
+ width: 180px;
+ height: 60px;
+ background: #fff;
+ border-radius: 50px;
+ box-shadow: 40px 10px 0 10px #fff, 90px 15px 0 5px #fff, 130px 5px 0 0 #fff;
+ opacity: 0.85;
+ animation: drift 48s linear infinite;
+}
+
+.cloud:before,
+.cloud:after {
+ content: "";
+ position: absolute;
+ background: #fff;
+ border-radius: 50%;
+}
+
+.cloud:before {
+ width: 60px;
+ height: 60px;
+ top: -25px;
+ left: 20px;
+}
+
+.cloud:after {
+ width: 80px;
+ height: 80px;
+ top: -35px;
+ left: 70px;
+}
+
+.cloud.c1 {
+ top: 20%;
+ left: -200px;
+ animation-duration: 52s;
+}
+
+.cloud.c2 {
+ top: 35%;
+ left: -320px;
+ animation-duration: 60s;
+ animation-delay: -10s;
+ transform: scale(1.2);
+}
+
+.cloud.c3 {
+ top: 55%;
+ left: -260px;
+ animation-duration: 56s;
+ animation-delay: -20s;
+ transform: scale(0.9);
+}
+
+@keyframes drift {
+ 0% {
+ transform: translateX(0);
+ }
+ 100% {
+ transform: translateX(140%);
+ }
+}
+
#timer {
+ position: absolute;
+ top: 0;
+ left: 0;
width: 100%;
height: 100%;
line-height: 100%;
- background: #000;
color: #fff;
text-align: center;
font-weight: 700;
- font-family: century gothic;
+ font-family: century gothic, "Helvetica Neue", Arial, sans-serif;
+ text-shadow: 0 0 10px rgba(0, 0, 0, 0.35);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none;
+ cursor: pointer;
+ z-index: 5;
}
+
#hide,
#toggle,
#reset,
@@ -43,20 +221,25 @@ body {
width: 170px;
height: 44px;
line-height: 30px;
+ z-index: 10;
}
+
#reset {
margin-left: 50px;
}
+
#hide {
margin-left: -30px;
width: 70px;
}
+
#min10,
#min60,
#add10,
#add60 {
width: 50px;
}
+
#def10,
#def30,
#def60,
@@ -69,48 +252,98 @@ body {
top: auto;
width: 70px;
}
+
+#custom-countdown-form {
+ position: absolute;
+ bottom: 55px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 6px 10px;
+ border-radius: 6px;
+ background: rgba(255, 255, 255, 0.85);
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.18);
+ z-index: 12;
+}
+
+#custom-countdown {
+ width: 150px;
+ border: 1px solid #b1c5d6;
+ border-radius: 4px;
+ padding: 6px 8px;
+ font-size: 14px;
+}
+
+#apply-countdown {
+ height: 32px;
+ line-height: 18px;
+ padding: 6px 12px;
+}
+
+#countdown-status {
+ min-width: 120px;
+ font-size: 13px;
+ color: #2b6e3f;
+ font-weight: 600;
+}
+
#min10 {
margin-left: -270px;
}
+
#min60 {
margin-left: -330px;
}
+
#add10 {
margin-left: 230px;
}
+
#add60 {
margin-left: 290px;
}
+
#def10 {
margin-left: -285px;
}
+
#def30 {
margin-left: -205px;
}
+
#def60 {
margin-left: -125px;
}
+
#def180 {
margin-left: -45px;
}
+
#def300 {
margin-left: 35px;
}
+
#def600 {
margin-left: 115px;
}
+
#def900 {
margin-left: 195px;
}
+
#def1800 {
margin-left: 275px;
}
+
#audio {
position: absolute;
bottom: 30px;
right: 10px;
z-index: 10;
}
+
#xxx {
position: absolute;
top: 10px;
diff --git a/index.html b/index.html
index fbd0173..9d93cee 100644
--- a/index.html
+++ b/index.html
@@ -1 +1,44 @@
-
????
-60-10RUNRESETൠ+10+6010 sec30 sec1 min3 min5 min10 min15 min30 min
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+ -60
+ -10
+ RUN
+ RESET
+ ൠ
+ +10
+ +60
+ 10 sec
+ 30 sec
+ 1 min
+ 3 min
+
+ 5 min
+ 10 min
+ 15 min
+ 30 min
+
+
diff --git a/index.js b/index.js
index dd7b28e..993466c 100644
--- a/index.js
+++ b/index.js
@@ -1,5 +1,5 @@
// Generated by LiveScript 1.3.1
-var start, isBlink, isLight, isRun, isShow, isWarned, handler, latency, stopBy, delay, audioRemind, audioEnd, newAudio, soundToggle, show, adjust, toggle, reset, blink, count, run, resize;
+var start, isBlink, isLight, isRun, isShow, isWarned, handler, latency, stopBy, delay, audioRemind, sunWrap, sun, sunRays, sunDuration, roosterTimers, rooster, roosterShown, newAudio, soundToggle, show, formatTime, adjust, parseTimeInput, setStatusMessage, setCustomCountdown, setPresetCountdown, toggle, reset, blink, count, run, resize, updateSunPosition, updateSunAppearance, updateSunSize, startRoosterCountdown, triggerRoosterCrow, stopRoosterCrow, hideRooster;
start = null;
isBlink = false;
isLight = true;
@@ -11,7 +11,13 @@ latency = 0;
stopBy = null;
delay = 60000;
audioRemind = null;
-audioEnd = null;
+sunWrap = null;
+sun = null;
+sunRays = null;
+sunDuration = 0;
+roosterTimers = [];
+rooster = null;
+roosterShown = false;
newAudio = function(file){
var x$, node;
x$ = node = new Audio();
@@ -36,9 +42,38 @@ show = function(){
isShow = !isShow;
return $('.fbtn').css('opacity', isShow ? '1.0' : '0.1');
};
+formatTime = function(ms){
+ var totalSeconds, hours, minutes, seconds, parts;
+ totalSeconds = Math.max(0, Math.ceil(ms / 1000));
+ hours = Math.floor(totalSeconds / 3600);
+ minutes = Math.floor(totalSeconds % 3600 / 60);
+ seconds = totalSeconds % 60;
+ parts = [];
+ if (hours > 0) {
+ parts.push(hours);
+ }
+ parts.push((hours > 0 ? ('0' + minutes).slice(-2) : minutes));
+ parts.push(('0' + seconds).slice(-2));
+ return parts.join(':');
+};
adjust = function(it, v){
if (isBlink) {
- return;
+ if (handler) {
+ clearInterval(handler);
+ }
+ handler = null;
+ start = null;
+ latency = 0;
+ stopBy = null;
+ isBlink = false;
+ isLight = true;
+ $('#timer').css('color', '#fff');
+ isRun = false;
+ $('#toggle').text("RUN");
+ isWarned = false;
+ stopRoosterCrow();
+ hideRooster();
+ soundToggle(audioRemind, false);
}
delay = delay + it * 1000;
if (it === 0) {
@@ -47,7 +82,86 @@ adjust = function(it, v){
if (delay <= 0) {
delay = 0;
}
- $('#timer').text(delay);
+ $('#timer').text(formatTime(delay));
+ if (!isRun) {
+ sunDuration = delay;
+ updateSunPosition(delay);
+ }
+ return resize();
+};
+parseTimeInput = function(input){
+ var parts, sec, min, hour;
+ input = input != null ? input.trim() : '';
+ if (!input) {
+ return null;
+ }
+ if (input.indexOf(':') !== -1) {
+ parts = input.split(':');
+ if (parts.length === 2) {
+ min = parseInt(parts[0], 10);
+ sec = parseInt(parts[1], 10);
+ if (isNaN(min) || isNaN(sec)) {
+ return null;
+ }
+ return min * 60 + sec;
+ } else if (parts.length === 3) {
+ hour = parseInt(parts[0], 10);
+ min = parseInt(parts[1], 10);
+ sec = parseInt(parts[2], 10);
+ if (isNaN(hour) || isNaN(min) || isNaN(sec)) {
+ return null;
+ }
+ return hour * 3600 + min * 60 + sec;
+ } else {
+ return null;
+ }
+ }
+ sec = parseFloat(input);
+ if (isNaN(sec)) {
+ return null;
+ }
+ return sec;
+};
+setStatusMessage = function(message, isError){
+ var status;
+ status = $('#countdown-status');
+ if (!status.length) {
+ return;
+ }
+ status.text(message || '');
+ status.css('color', isError ? '#b00020' : '#2b6e3f');
+};
+setCustomCountdown = function(){
+ var input, seconds;
+ input = $('#custom-countdown');
+ if (!input.length) {
+ return;
+ }
+ seconds = parseTimeInput(input.val());
+ if (seconds == null || seconds < 0) {
+ setStatusMessage('請輸入正確時間', true);
+ return;
+ }
+ if (isRun) {
+ toggle();
+ }
+ adjust(0, seconds);
+ setStatusMessage('已設定 ' + seconds + ' 秒');
+ return input.select();
+};
+setPresetCountdown = function(seconds){
+ if (seconds == null || seconds < 0) {
+ return;
+ }
+ adjust(0, seconds);
+ sunDuration = delay;
+ updateSunPosition(delay);
+ if (!isRun) {
+ return toggle();
+ }
+ start = new Date();
+ latency = 0;
+ stopBy = null;
return resize();
};
toggle = function(){
@@ -57,7 +171,8 @@ toggle = function(){
stopBy = new Date();
clearInterval(handler);
handler = null;
- soundToggle(audioEnd, false);
+ stopRoosterCrow();
+ hideRooster();
soundToggle(audioRemind, false);
}
if (stopBy) {
@@ -72,7 +187,8 @@ reset = function(){
delay = 1000;
}
soundToggle(audioRemind, false);
- soundToggle(audioEnd, false);
+ stopRoosterCrow();
+ hideRooster();
stopBy = 0;
isWarned = false;
isBlink = false;
@@ -84,8 +200,10 @@ reset = function(){
clearInterval(handler);
}
handler = null;
- $('#timer').text(delay);
+ $('#timer').text(formatTime(delay));
$('#timer').css('color', '#fff');
+ sunDuration = delay;
+ updateSunPosition(delay);
return resize();
};
blink = function(){
@@ -97,6 +215,7 @@ count = function(){
var tm, diff;
tm = $('#timer');
diff = start.getTime() - new Date().getTime() + delay + latency;
+ updateSunPosition(Math.max(0, diff));
if (diff > 60000) {
isWarned = false;
}
@@ -108,15 +227,20 @@ count = function(){
soundToggle(audioRemind, false);
}
if (diff < 0 && !isBlink) {
- soundToggle(audioEnd, true);
+ if (!roosterShown) {
+ startRoosterCountdown();
+ }
+ updateSunPosition(0);
isBlink = true;
diff = 0;
clearInterval(handler);
handler = setInterval(function(){
return blink();
}, 500);
+ } else if (diff <= 0 && !roosterShown) {
+ startRoosterCountdown();
}
- tm.text(diff + "");
+ tm.text(formatTime(diff));
return resize();
};
run = function(){
@@ -124,6 +248,8 @@ run = function(){
start = new Date();
latency = 0;
isBlink = false;
+ sunDuration = delay;
+ updateSunPosition(delay);
}
if (handler) {
clearInterval(handler);
@@ -139,20 +265,111 @@ run = function(){
}
};
resize = function(){
- var tm, w, h, len;
+ var tm, w, h, len, fontSize;
tm = $('#timer');
w = tm.width();
h = $(window).height();
len = tm.text().length;
len >= 3 || (len = 3);
tm.css('font-size', 1.5 * w / len + "px");
- return tm.css('line-height', h + "px");
+ tm.css('line-height', h + "px");
+ fontSize = parseFloat(tm.css('font-size')) || 0;
+ return updateSunSize(fontSize);
+};
+updateSunPosition = function(remaining){
+ var progress, target;
+ if (!sunWrap || !sun || sunDuration <= 0) {
+ return;
+ }
+ progress = 1 - remaining / sunDuration;
+ if (progress < 0) {
+ progress = 0;
+ }
+ if (progress > 1) {
+ progress = 1;
+ }
+ target = -60 + progress * 110;
+ sunWrap.style.bottom = target + "vh";
+ return updateSunAppearance(progress);
+};
+updateSunAppearance = function(progress){
+ var scale, rayScale, glowStrength;
+ if (!sun || !sunRays) {
+ return;
+ }
+ scale = 1.1 + progress * 0.9;
+ rayScale = 1 + progress * 0.8;
+ glowStrength = 70 + progress * 90;
+ sun.style.transform = "scale(" + scale + ")";
+ sun.style.boxShadow = "0 0 " + glowStrength + "px rgba(255, 193, 55, 0.9), 0 0 " + glowStrength * 1.8 + "px rgba(255, 193, 55, 0.5)";
+ sunRays.style.opacity = 0.25 + progress * 0.7 + "";
+ return sunRays.style.transform = "scale(" + rayScale + ")";
+};
+updateSunSize = function(fontSize){
+ var root, sunSize, raySize;
+ if (!fontSize) {
+ return;
+ }
+ root = document.documentElement.style;
+ sunSize = Math.max(70, Math.min(fontSize * 0.4, 220));
+ raySize = sunSize * 1.8;
+ root.setProperty('--sun-size', sunSize + "px");
+ return root.setProperty('--sun-ray-size', raySize + "px");
+};
+startRoosterCountdown = function(){
+ if (roosterShown || !rooster) {
+ return;
+ }
+ roosterShown = true;
+ rooster.classList.add('show');
+ return triggerRoosterCrow();
+};
+triggerRoosterCrow = function(){
+ stopRoosterCrow();
+ rooster.classList.add('flap');
+ return roosterTimers.push(setTimeout(function(){
+ return hideRooster();
+ }, 1700));
+};
+stopRoosterCrow = function(){
+ var i$;
+ for (i$ = 0; i$ < roosterTimers.length; ++i$) {
+ clearTimeout(roosterTimers[i$]);
+ }
+ if (rooster) {
+ rooster.classList.remove('flap');
+ }
+ return roosterTimers = [];
+};
+hideRooster = function(){
+ roosterShown = false;
+ stopRoosterCrow();
+ if (rooster) {
+ return rooster.classList.remove('show');
+ }
};
window.onload = function(){
- $('#timer').text(delay);
+ $('#timer').text(formatTime(delay));
+ sunWrap = document.getElementById('sun-wrap');
+ sun = document.getElementById('sun');
+ sunRays = document.getElementById('sun-rays');
+ rooster = document.getElementById('rooster');
+ if (rooster) {
+ rooster.addEventListener('animationend', function(e){
+ if (e.animationName === 'flap') {
+ return hideRooster();
+ }
+ });
+ }
+ sunDuration = delay;
+ updateSunPosition(delay);
resize();
- audioRemind = newAudio('audio/smb_warning.mp3');
- return audioEnd = newAudio('audio/smb_mariodie.mp3');
+ $('#custom-countdown').on('keypress', function(e){
+ if (e.key === 'Enter') {
+ return setCustomCountdown();
+ }
+ });
+ return audioRemind = newAudio('audio/smb_warning.mp3');
};
window.onresize = function(){
return resize();
diff --git a/index.pug b/index.pug
index 7d258aa..3e934c1 100644
--- a/index.pug
+++ b/index.pug
@@ -8,7 +8,15 @@ html
script(type="text/javascript",src="bootstrap3.min.js")
script(type="text/javascript",src="index.js")
body
- #timer ????
+ #sky
+ #sun-wrap
+ #sun-rays
+ #sun
+ #rooster(aria-hidden="true") 🐦
+ .cloud.c1
+ .cloud.c2
+ .cloud.c3
+ #timer ????
a#min60.btn.btn-info.fbtn(onclick="adjust(-60)") -60
a#min10.btn.btn-info.fbtn(onclick="adjust(-10)") -10
a#toggle.btn.btn-primary(onclick="toggle()") RUN
@@ -16,11 +24,11 @@ html
a#hide.btn.btn-default(onclick="show()") ൠ
a#add10.btn.btn-info.fbtn(onclick="adjust(10)") +10
a#add60.btn.btn-info.fbtn(onclick="adjust(60)") +60
- a#def10.btn.btn-success.fbtn(onclick="adjust(0,10)") 10 sec
- a#def30.btn.btn-success.fbtn(onclick="adjust(0,30)") 30 sec
- a#def60.btn.btn-success.fbtn(onclick="adjust(0,60)") 1 min
- a#def180.btn.btn-success.fbtn(onclick="adjust(0,180)") 3 min
- a#def300.btn.btn-success.fbtn(onclick="adjust(0,300)") 5 min
- a#def600.btn.btn-success.fbtn(onclick="adjust(0,600)") 10 min
- a#def900.btn.btn-success.fbtn(onclick="adjust(0,900)") 15 min
- a#def1800.btn.btn-success.fbtn(onclick="adjust(0,1800)") 30 min
+ a#def10.btn.btn-success.fbtn(onclick="setPresetCountdown(10)") 10 sec
+ a#def30.btn.btn-success.fbtn(onclick="setPresetCountdown(30)") 30 sec
+ a#def60.btn.btn-success.fbtn(onclick="setPresetCountdown(60)") 1 min
+ a#def180.btn.btn-success.fbtn(onclick="setPresetCountdown(180)") 3 min
+ a#def300.btn.btn-success.fbtn(onclick="setPresetCountdown(300)") 5 min
+ a#def600.btn.btn-success.fbtn(onclick="setPresetCountdown(600)") 10 min
+ a#def900.btn.btn-success.fbtn(onclick="setPresetCountdown(900)") 15 min
+ a#def1800.btn.btn-success.fbtn(onclick="setPresetCountdown(1800)") 30 min
diff --git a/index.styl b/index.styl
index bdf0e21..be835be 100644
--- a/index.styl
+++ b/index.styl
@@ -9,16 +9,94 @@ html, body
-moz-user-select: moz-none
-ms-user-select: none
user-select: none
+ overflow: hidden
+
+#sky
+ position: relative
+ width: 100%
+ height: 100%
+ background: linear-gradient(180deg, #8fd3ff 0%, #cfefff 60%, #f7fbff 100%)
+ overflow: hidden
+
+#sun
+ position: absolute
+ left: 50%
+ bottom: -25vh
+ width: 140px
+ height: 140px
+ margin-left: -70px
+ border-radius: 50%
+ background: radial-gradient(circle at 30% 30%, #fff7c7, #ffd25a 60%, #ffba29 100%)
+ box-shadow: 0 0 40px rgba(255, 188, 38, 0.7), 0 0 80px rgba(255, 188, 38, 0.35)
+ transition: bottom 0.4s linear
+
+.cloud
+ position: absolute
+ top: 15%
+ width: 180px
+ height: 60px
+ background: #fff
+ border-radius: 50px
+ box-shadow: 40px 10px 0 10px #fff, 90px 15px 0 5px #fff, 130px 5px 0 0 #fff
+ opacity: 0.85
+ animation: drift 48s linear infinite
+ &:before, &:after
+ content: ""
+ position: absolute
+ background: #fff
+ border-radius: 50%
+ &:before
+ width: 60px
+ height: 60px
+ top: -25px
+ left: 20px
+ &:after
+ width: 80px
+ height: 80px
+ top: -35px
+ left: 70px
+
+.cloud.c1
+ top: 20%
+ left: -200px
+ animation-duration: 52s
+
+.cloud.c2
+ top: 35%
+ left: -320px
+ animation-duration: 60s
+ animation-delay: -10s
+ transform: scale(1.2)
+
+.cloud.c3
+ top: 55%
+ left: -260px
+ animation-duration: 56s
+ animation-delay: -20s
+ transform: scale(0.9)
+
+@keyframes drift
+ 0%
+ transform: translateX(0)
+ 100%
+ transform: translateX(140%)
#timer
+ position: absolute
+ top: 0
+ left: 0
width: 100%
height: 100%
line-height: 100%
- background: #000
color: #fff
text-align: center
font-weight: 700
- font-family: century gothic
+ font-family: century gothic, "Helvetica Neue", Arial, sans-serif
+ text-shadow: 0 0 10px rgba(0, 0, 0, 0.35)
+ display: flex
+ align-items: center
+ justify-content: center
+ pointer-events: none
#hide, #toggle, #reset, #min10, #min60, #add10, #add60, #def10, #def30, #def60, #def180, #def300, #def600, #def900, #def1800
position: absolute
@@ -28,6 +106,7 @@ html, body
width: 170px
height: 44px
line-height: 30px
+ z-index: 10
#reset
margin-left: 50px
#hide