Animation framework using JS + Improvements in Quiz UX

This commit is contained in:
Niko 2022-02-25 13:27:25 +01:00
parent 452837cf55
commit e973da1aa8
7 changed files with 264 additions and 48 deletions

View File

@ -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;

View File

@ -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 }

View File

@ -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);

View File

@ -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 `<i class="fa fa-${icon} fa-fw"></i>` }

View File

@ -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');
}
}
},
}

View File

@ -1,46 +1,56 @@
<form id="quiz" style="padding: 0"
class="panel flex center item">
<form id="quiz" class="panel flex center item"
style="padding: 0; background: none; box-shadow: none">
<div class="content center">
<div class="person">
<div class="lovebg flex">
<div class="width-max">
<span class="bigname">{.from.name}</span>
<br>
<span class="acct" style="
position: relative;
top: 2px;
">@{.from.acct}</span>
<br><br>
<span>
<div class="lovebg flex not-visible-scroll">
<div class="width-max">
<span class="bigname">{.from.name}</span>
<br>
<span class="acct" style="
position: relative;
top: 2px;
">@{.from.acct}</span>
<br><br>
<b>{.from.props.age}</b> {s:app.years_old},
<span style="opacity: .6">
<i class="fa fa-map-marker fa-fw">
</i>&nbsp;{.from.props.place}
</span>
</span>
</div>
<div>
<a href="#profile/{.from._id}">
<div class="big avatar round">
<img class="round" src="{.from.avatar.url}"/>
</div>
</a>
<div class="avatar-container">
<a href="#profile/{.from._id}">
<div class="big avatar round">
<img class="round" src="{.from.avatar.url}"/>
</div>
</a>
</div>
</div>
<div class="lovebg flex visible-scroll"
style="padding: .5em 1em">
<div class="flex width-max">
<div class="flex width-max">
<span class="center-v bigname">{.from.name}</span>
</div>
<div class="avatar-container">
<div class="avatar round">
<img class="round" src="{.from.avatar.url}"/>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="quiz-content">
<h2 style="
color: var(--clr_quiz);
padding: 0;
margin-right: .8em;
text-align: center;
">
<i class="fa fa-quote-right fa-fw">
</i>{s:app.fill_your_crush_quiz}
</h2>
<h3 class="text-center"
<h3 class="text-center" style="color: #0000005e"
>{.content.length} {s:app.questions}</h3>
<br><br>
<br>
<div id="questions"></div>
</div>
</div>

View File

@ -7,21 +7,22 @@
</tr></tbody>
</table>
<div class="type-freetext">
<input class="width-max" type="text"
<input class="card-item width-max" type="text"
name="question_{.index}"/>
</div>
<div class="type-options">
<fieldset class="flex">
<fieldset>
{loop:options}
<label class="width-max">
<label class="card-item button rounder flex">
<div class="center">
<input type="radio" name="question_{.index}"
value="{.item}">
<span>{.item_cap}</span>
</div>
</label>
{/loop}
</fieldset>
</div>
<br><br>
<hr style="max-width:100%">
<br>
<hr style="max-width:100%">
</div>