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