From e973da1aa8f709a28c6b6ac1c809fa9c612a4d06 Mon Sep 17 00:00:00 2001 From: Niko Date: Fri, 25 Feb 2022 13:27:25 +0100 Subject: [PATCH] Animation framework using JS + Improvements in Quiz UX --- web/src/app/css/app.css | 37 ++++++++++-- web/src/app/css/base.css | 30 +++++++--- web/src/app/js/app.js | 36 ++++++++++- web/src/app/js/base.js | 66 +++++++++++++++++++- web/src/app/js/pages/quiz.js | 76 +++++++++++++++++++++++- web/src/app/js/templates/quiz/index.html | 56 ++++++++++------- web/src/app/js/templates/quiz/item.html | 11 ++-- 7 files changed, 264 insertions(+), 48 deletions(-) diff --git a/web/src/app/css/app.css b/web/src/app/css/app.css index ca7f1e7..a25649d 100644 --- a/web/src/app/css/app.css +++ b/web/src/app/css/app.css @@ -32,7 +32,6 @@ input[type="radio"] { height: 1.2em; margin-right: .4em; vertical-align: middle; - } fieldset { border: none; @@ -138,6 +137,9 @@ main { background: #fff; border-radius: 6px; } +.panel input.card-item { + padding: .5em !important; +} .button span { user-select: none; @@ -166,8 +168,8 @@ main { } #quizs .avatar.round.big, #quiz .avatar.round.big { - width: 5em !important; - height: 5em !important; + width: 5em; + height: 5em; } #quizs .avatar img, #quiz .avatar img { @@ -177,8 +179,8 @@ main { } #quizs .avatar.big img, #quiz .avatar.big img { - width: 5em !important; - height: 5em !important; + width: 5em; + height: 5em; } #quizs .metadata .count { float: right; @@ -208,11 +210,36 @@ main { background-repeat: no-repeat; background-position: right; } +#quiz .person.scrolled .not-visible-scroll, +#quiz .person:not(.scrolled) .visible-scroll { + display: none; +} +#quiz .person.scrolled { + border-bottom: 5px solid #00000014; +} +#quiz .person.scrolled .avatar-container { + position: relative; + left: -1em; +} +#quiz .person.scrolled .avatar { + width: 3em !important; + height: 3em !important; +} +#quiz .person.scrolled .avatar img { + width: 3em !important; + height: 3em !important; +} #quiz #questions { padding: 0 .8em; max-width: 32em; margin: auto; } +#quiz #questions .type-options label { + padding: .5em 1em; + margin: .2em; + width: max-content; + float: left; +} #quiz .quiz-content { padding: 1.5em 1em; background: #fff; diff --git a/web/src/app/css/base.css b/web/src/app/css/base.css index 3f907f8..3d2504d 100644 --- a/web/src/app/css/base.css +++ b/web/src/app/css/base.css @@ -2,13 +2,6 @@ body { font-family: system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,"mastodon-font-sans-serif",sans-serif; } -input[type="text"] { - border: 1px solid #00000026; - padding: .5em; - box-shadow: 0px 0px .5em #0000000f; - border-radius: 6px; -} - .button, a[onclick], .pointer { cursor: pointer; @@ -20,6 +13,8 @@ input[type="text"] { .flex { display: flex } .flex > .center { margin: auto } +.flex > .center-v { margin: auto 0 } +.flex > .center-h { margin: 0 auto } .width-max { width: 100% } .height-max { height: 100% } @@ -63,9 +58,20 @@ input[type="text"] { @media (max-width: 400px) { .media-max400 { display: none } } @media (max-width: 500px) { .media-max500 { display: none } } + +.slow { animation-duration: 2s !important } +.mid { animation-duration: 1s !important } +.slide-to-top { + position: relative !important; + animation: animate2top 0.1s; +} +.slide-from-top { + position: relative !important; + animation: animatetop 0.1s; +} .slide-to-right { position: relative !important; - animation. animate2right 0.1s; + animation: animate2right 0.1s; } .slide-from-right { position: relative !important; @@ -79,6 +85,14 @@ input[type="text"] { position: relative !important; animation: animateleft 0.1s; } +@keyframes animate2top { + from { top: 0; opacity: 1 } + to { top: -200px; opacity: .5 } +} +@keyframes animatetop { + from { top: -200px; opacity: .5 } + to { top: 0; opacity: 1 } +} @keyframes animate2right { from { right: 0; opacity: 1 } to { right: -200px; opacity: .5 } diff --git a/web/src/app/js/app.js b/web/src/app/js/app.js index 1036bea..402fa2c 100644 --- a/web/src/app/js/app.js +++ b/web/src/app/js/app.js @@ -447,6 +447,7 @@ app.toast = { } function scriptPageHandler(modname, cfg) { + cfg['module'] = modname cfg['callback'] = function(args) { app.script(modname, function(ok) { if (!ok) return; @@ -505,10 +506,41 @@ window.onhashchange = function(e) { for (var i = 0; i < app.hashHandlers.length; i++) { const cfg = app.hashHandlers[i]; if (cfg.exact !== undefined && - cfg.exact === path) + cfg.exact === path) { + window._currentPage = cfg.module; return cfg.callback(args); + } if (cfg.regex !== undefined && - path.match(cfg.regex)) + path.match(cfg.regex)) { + window._currentPage = cfg.module; return cfg.callback(args); + } } + window._currentPage = null; } + +var _currentPage = null; +function page() { + if (window._currentPage === null) + return null; + return eval(`app.${window._currentPage}`); +} + +function scroll() { return Math.floor(window.scrollY) } +window.onscroll = function(e) { + if (page() === null || page().onScroll === undefined) + return; + if (scroll() % 10 !== 0) + return; + if (scroll() === 0) + setTimeout(async function() { + if (scroll() === 0) { + await page().onScroll(0, true); + window.scrollTo(0,0); + } + }, 500); + else page().onScroll(scroll(), false); +} +// TODO: remove later, we suspect is not needed +//window.ontouchmove = window.onscroll; +//window.addEventListener('touchmove', window.onscroll); diff --git a/web/src/app/js/base.js b/web/src/app/js/base.js index fe75228..e467a52 100644 --- a/web/src/app/js/base.js +++ b/web/src/app/js/base.js @@ -17,6 +17,14 @@ function dragElement(elmnt, options, onStopDrag) { const isMobile = e.touches !== undefined; const cx = !isMobile ? e.clientX : e.touches[0].clientX * 1.3; const cy = !isMobile ? e.clientY : e.touches[0].clientY * 1.3; + if (options.borderDetectPx !== undefined) { + const bleft = cx - elmnt.offsetLeft; + const bright = elmnt.offsetWidth - cx; + if (bright > options.borderDetectPx && options.right === false) + return; + if (bleft > options.borderDetectPx && options.left === false) + return; + } // get the mouse cursor position at startup: if (options.x === undefined || options.x === true) pos3 = cx; @@ -88,9 +96,63 @@ function animateTimeout(elem, style, time) { return; if (typeof elem === 'string') elem = document.querySelector(elem); - elem.classList.add(style); + if (style instanceof Array) + for (var i in style) + elem.classList.add(style[i]); + else elem.classList.add(style); setTimeout(function() { - elem.classList.remove(style) }, time); + if (style instanceof Array) + for (var i in style) + elem.classList.remove(style[i]); + else elem.classList.remove(style); + }, time); +} + +async function animateJS(elem, stages, removeAfter) { + if (typeof elem === 'string') + elem = document.querySelector(elem); + if (elem === undefined || elem === null) + return; + var countFin = 0; + for (var i = 0; i < stages.length; i++) { + const stage = stages[i]; + const process = async function() { + if (stage.range !== undefined) { + const from = stage.range[0]; + const to = stage.range[1]; + var step = stage.step || 10; + step = Math.abs(step); + if (to < from) + step = step - (step * 2); + const diff = (to > from) ? to - from : from - to; + const times = Math.ceil(Math.abs(diff) / Math.abs(step)); + const tpl = stage.value || '{}'; + const slp = stage.sleep || 10; + for (var j = 1; j < times; j++) { + elem.style[stage.prop] = tpl + .replaceAll('{}', from + (j*step)); + await sleep(slp); + } + elem.style[stage.prop] = tpl + .replaceAll('{}', to); + } else + elem.style[stage.prop] = stage.value; + countFin++; + }; + if (stage.async) process(); + else await process(); + } + + var rcp = true; + setTimeout(function() { rcp = false },10*1000); + while (rcp) { + if (countFin >= stages.length) + break; + await sleep(2); + } + if (removeAfter) + for (var i = 0; i < stages.length; i++) + elem.style.removeProperty(stages[i].prop); } function fa(icon) { return `` } diff --git a/web/src/app/js/pages/quiz.js b/web/src/app/js/pages/quiz.js index 88ae5da..58906ca 100644 --- a/web/src/app/js/pages/quiz.js +++ b/web/src/app/js/pages/quiz.js @@ -19,11 +19,12 @@ app.pages.quiz = { const onDataLoaded = async function(json) { await app.template.loadMany(['quiz.index', 'quiz.item']); - app.pages.quiz.data = json; - app.pages.quiz.paint(json); + page().data = json; + page().paint(json); if (getNormalizedURI() === app.vars.app_dir && window.prevHash === '') animateTimeout('#quiz', 'slide-from-right'); + page().setDragEvent(); }; if (data === undefined) http.get(`/api/v1/me/quizs?id=${args[1]}`, @@ -31,7 +32,7 @@ app.pages.quiz = { else onDataLoaded(data); }, paint: function(json) { - json = json || app.pages.quiz.data; + json = json || page().data; if (json.from.props.age === undefined) json.from.props.age = '??'; if (json.from.props.gender === undefined) @@ -70,5 +71,74 @@ app.pages.quiz = { document.querySelector(`#quiz #question-${i} .type-options`).remove(); } } + page().scrollInit(); + }, + setDragEvent: function() { + const element = document.querySelector('#quiz .content'); + element.style.position = 'relative'; + var options = { + y: false, + left: false, + rotate: true, + fadeOut: true, + return2start: true, + borderDetectPx: 30, + permitUserSelect: true, + }; + dragElement(element, options, function(a1, a2) { + const ix = a1[0]; + const fx = a2[0]; + if (ix < fx && fx - ix > 200) { + animateTimeout(element, 'slide-to-right', 100); + window.history.back(); + } else page().setDragEvent(); + }); + }, + _scrollData: {}, + scrollInit: function() { + const elem = document.querySelector('#quiz .person'); + if (elem === null) return; + page()._scrollData.state1h = elem.offsetHeight; + elem.classList.add('scrolled'); + page()._scrollData.state2h = elem.offsetHeight; + elem.classList.remove('scrolled'); + }, + onScroll: async function(scroll, isTop) { + // TODO: change to a isMobile function + if (document.body.offsetWidth < 610) + { + const elem = document.querySelector('#quiz .person'); + if (elem === null) return; + + const cfg = { sleep: 5, step: 5 } + if (!isTop) { + if (elem.classList.contains('scrolled')) + return; + const top1 = page()._scrollData.state1h - page()._scrollData.state2h; + await animateJS(elem, [ + { prop: 'position', value: 'fixed' }, + { prop: 'width', value: '100%' }, + { prop: 'top', range: [0,-top1], value: '{}px', + step: cfg.step, sleep: cfg.sleep }, + ], true); + elem.classList.add('scrolled'); + } else { + if (!elem.classList.contains('scrolled')) + return; + const elemi = elem.querySelector('.visible-scroll'); + const h1 = page()._scrollData.state2h - 5; + const h2 = page()._scrollData.state1h - 5; + elemi.style.height = '90%'; + await animateJS(elem, [ + { prop: 'position', value: 'fixed' }, + { prop: 'width', value: '100%' }, + { prop: 'top', value: '0' }, + { prop: 'height', range: [h1,h2], value: '{}px', + step: cfg.step, sleep: cfg.sleep }, + ], true); + elemi.style.removeProperty('height'); + elem.classList.remove('scrolled'); + } + } }, } diff --git a/web/src/app/js/templates/quiz/index.html b/web/src/app/js/templates/quiz/index.html index 9e498b6..7f1c9dc 100644 --- a/web/src/app/js/templates/quiz/index.html +++ b/web/src/app/js/templates/quiz/index.html @@ -1,46 +1,56 @@ -
+
-
-
- {.from.name} -
- @{.from.acct} -

- +
+
+ {.from.name} +
+ @{.from.acct} +

{.from.props.age} {s:app.years_old},  {.from.props.place} - -
- +
+
+
+ {.from.name} +
+
+
+ +
+
+
-

{s:app.fill_your_crush_quiz}

-

{.content.length} {s:app.questions}

-

+
diff --git a/web/src/app/js/templates/quiz/item.html b/web/src/app/js/templates/quiz/item.html index 6332a86..526be76 100644 --- a/web/src/app/js/templates/quiz/item.html +++ b/web/src/app/js/templates/quiz/item.html @@ -7,21 +7,22 @@
-
-
+
{loop:options} -
-

-

+