Skip to content
Snippets Groups Projects
Commit 63833c36 authored by Xiao Gui's avatar Xiao Gui
Browse files

WIP: adapt to mobile

parent 7d58c1ea
No related branches found
No related tags found
No related merge requests found
Showing
with 281 additions and 172 deletions
.btnWrapper
{
display:flex;
align-items: center;
justify-content: center;
}
.btnWrapper > .btn
{
width: 2.5em;
height: 2.5em;
display: flex;
align-items: center;
justify-content: center;
}
.btnWrapper.btnWrapper-lg > .btn
{
width: 3em;
height: 3em;
}
\ No newline at end of file
......@@ -5,12 +5,14 @@ import { WidgetUnit } from "src/atlasViewer/widgetUnit/widgetUnit.component";
import { LayerBrowser } from "src/ui/layerbrowser/layerbrowser.component";
import { DataBrowser } from "src/ui/databrowserModule/databrowser/databrowser.component";
import { PluginBannerUI } from "../pluginBanner/pluginBanner.component";
import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service";
@Component({
selector: 'menu-icons',
templateUrl: './menuicons.template.html',
styleUrls: [
'./menuicons.style.css'
'./menuicons.style.css',
'../btnShadow.style.css'
]
})
......@@ -37,9 +39,14 @@ export class MenuIconsBar{
pluginBanner: ComponentRef<PluginBannerUI> = null
pbWidget: ComponentRef<WidgetUnit> = null
get isMobile(){
return this.constantService.mobile
}
constructor(
private widgetServices:WidgetServices,
private injector:Injector,
private constantService:AtlasViewerConstantsServices,
cfr: ComponentFactoryResolver
){
this.dbcf = cfr.resolveComponentFactory(DataBrowser)
......
<logo-container>
</logo-container>
<div *ngIf="false" class="btn btn-sm btn-secondary rounded-circle">
<i class="fas fa-brain">
</i>
</div>
<div
[tooltip]="dataBrowserTitle"
placement="right"
(click)="clickSearch($event)"
[ngClass]="databrowserIsShowing ? 'btn-primary' : 'btn-secondary'"
class="btn btn-sm rounded-circle">
<i class="fas fa-search">
</i>
*ngIf="isMobile"
[ngClass]="isMobile ? 'btnWrapper-lg' : ''"
class="btnWrapper">
<div
class="shadow btn btn-sm btn-outline-secondary rounded-circle">
<i class="fas fa-bars">
</i>
</div>
</div>
<div
tooltip="Layer"
placement="right"
(click)="clickLayer($event)"
[ngClass]="layerbrowserIsShowing ? 'btn-primary' : 'btn-secondary'"
class="btn btn-sm rounded-circle">
<i class="fas fa-layer-group">
</i>
[ngClass]="isMobile ? 'btnWrapper-lg' : ''"
class="btnWrapper">
<div
[tooltip]="dataBrowserTitle"
placement="right"
(click)="clickSearch($event)"
[ngClass]="databrowserIsShowing ? 'btn-primary' : 'btn-secondary'"
class="shadow btn btn-sm rounded-circle">
<i class="fas fa-search">
</i>
</div>
</div>
<div
tooltip="Plugins"
(click)="clickPlugins($event)"
placement="right"
[ngClass]="pluginbrowserIsShowing ? 'btn-primary' : 'btn-secondary'"
class="btn btn-sm rounded-circle">
<i class="fas fa-tools">
</i>
[ngClass]="isMobile ? 'btnWrapper-lg' : ''"
class="btnWrapper">
<div
tooltip="Layer"
placement="right"
(click)="clickLayer($event)"
[ngClass]="layerbrowserIsShowing ? 'btn-primary' : 'btn-secondary'"
class="shadow btn btn-sm rounded-circle">
<i class="fas fa-layer-group">
</i>
</div>
</div>
<div
[ngClass]="isMobile ? 'btnWrapper-lg' : ''"
class="btnWrapper">
<div
tooltip="Plugins"
(click)="clickPlugins($event)"
placement="right"
[ngClass]="pluginbrowserIsShowing ? 'btn-primary' : 'btn-secondary'"
class="shadow btn btn-sm rounded-circle">
<i class="fas fa-tools">
</i>
</div>
</div>
\ No newline at end of file
import { Component, Input, Output,EventEmitter, ElementRef, ViewChild, AfterViewInit, ChangeDetectionStrategy, OnDestroy } from "@angular/core";
import { Component, Input, Output,EventEmitter, ElementRef, ViewChild, AfterViewInit, ChangeDetectionStrategy, OnDestroy, OnInit, OnChanges } from "@angular/core";
import { fromEvent, Subject, Observable, merge, concat, of, combineLatest } from "rxjs";
import { map, switchMap, takeUntil, filter, scan, take } from "rxjs/operators";
import { map, switchMap, takeUntil, filter, scan, take, tap } from "rxjs/operators";
import { clamp } from "src/util/generator";
@Component({
selector : 'mobile-overlay',
......@@ -24,15 +25,15 @@ div:not(.active) > span:before
display: inline-block;
}
`
],
changeDetection: ChangeDetectionStrategy.OnPush
]
})
export class MobileOverlay implements AfterViewInit, OnDestroy{
export class MobileOverlay implements OnInit, OnDestroy{
@Input() tunableProperties : string [] = []
@Output() deltaValue : EventEmitter<{delta:number, selectedProp : string}> = new EventEmitter()
@ViewChild('initiator', {read: ElementRef}) initiator : ElementRef
@ViewChild('mobileMenuContainer', {read: ElementRef}) menuContainer : ElementRef
@ViewChild('intersector', {read: ElementRef}) intersector: ElementRef
private _onDestroySubject : Subject<boolean> = new Subject()
......@@ -42,25 +43,38 @@ export class MobileOverlay implements AfterViewInit, OnDestroy{
? this._focusedProperties
: this.tunableProperties[0]
}
get focusedIndex(){
return this._focusedProperties
? this.tunableProperties.findIndex(p => p === this._focusedProperties)
: 0
}
public showScreen$ : Observable<boolean>
public showProperties$ : Observable<boolean>
public showDelta$: Observable<boolean>
public showInitiator$: Observable<boolean>
private _drag$ : Observable<any>
private intersectionObserver: IntersectionObserver
ngOnDestroy(){
this._onDestroySubject.next(true)
this._onDestroySubject.complete()
}
ngAfterViewInit(){
ngOnInit(){
const config = {
root: this.intersector.nativeElement,
threshold: [...Array(10)].map((_, k) => k / 10)
}
this.showScreen$ = merge(
fromEvent(this.initiator.nativeElement, 'touchstart'),
fromEvent(this.initiator.nativeElement, 'touchend'),
).pipe(
map((ev:TouchEvent) => ev.touches.length === 1)
)
this.intersectionObserver = new IntersectionObserver((arg) => {
if (arg[0].isIntersecting) {
this.focusItemIndex = 2- Math.floor(arg[0].intersectionRatio * this.tunableProperties.length)
}
}, config)
this.intersectionObserver.observe(this.menuContainer.nativeElement)
this._drag$ = fromEvent(this.initiator.nativeElement, 'touchmove').pipe(
takeUntil(fromEvent(this.initiator.nativeElement, 'touchend').pipe(
filter((ev:TouchEvent) => ev.touches.length === 0)
......@@ -73,21 +87,58 @@ export class MobileOverlay implements AfterViewInit, OnDestroy{
filter(ev => ev.length === 2)
)
this.showProperties$ = fromEvent(this.initiator.nativeElement, 'touchstart').pipe(
switchMap(() => concat(
this._drag$.pipe(
map(double => ({
deltaX : double[1].touches[0].screenX - double[0].touches[0].screenX,
deltaY : double[1].touches[0].screenY - double[0].touches[0].screenY
})),
scan((acc, _curr) => acc),
map(v => v.deltaY ** 2 > v.deltaX ** 2)
),
of(false)
)
this.showProperties$ = concat(
of(false),
fromEvent(this.initiator.nativeElement, 'touchstart').pipe(
switchMap(() => concat(
this._drag$.pipe(
map(double => ({
deltaX : double[1].touches[0].screenX - double[0].touches[0].screenX,
deltaY : double[1].touches[0].screenY - double[0].touches[0].screenY
})),
scan((acc, _curr) => acc),
map(v => v.deltaY ** 2 > v.deltaX ** 2)
),
of(false)
))
)
)
this.showDelta$ = concat(
of(false),
fromEvent(this.initiator.nativeElement, 'touchstart').pipe(
switchMap(() => concat(
this._drag$.pipe(
map(double => ({
deltaX : double[1].touches[0].screenX - double[0].touches[0].screenX,
deltaY : double[1].touches[0].screenY - double[0].touches[0].screenY
})),
scan((acc, _curr) => acc),
map(v => v.deltaX ** 2 > v.deltaY ** 2)
),
of(false)
))
)
)
this.showInitiator$ = combineLatest(
this.showProperties$,
this.showDelta$
).pipe(
map(([flag1, flag2]) => !flag1 && !flag2)
)
this.showScreen$ = combineLatest(
merge(
fromEvent(this.initiator.nativeElement, 'touchstart'),
fromEvent(this.initiator.nativeElement, 'touchend')
),
this.showInitiator$
).pipe(
map(([ev, showInitiator] : [TouchEvent, boolean]) => showInitiator && ev.touches.length === 1)
)
fromEvent(this.initiator.nativeElement, 'touchstart').pipe(
switchMap(() => this._drag$.pipe(
map(double => ({
......@@ -130,7 +181,16 @@ export class MobileOverlay implements AfterViewInit, OnDestroy{
filter(v => v[0]),
map(v => v[1]),
takeUntil(this._onDestroySubject)
).subscribe(v => this.scrollHeight = v.deltaY)
).subscribe(v => {
const deltaY = v.deltaY
const cellHeight = this.menuContainer && this.tunableProperties && this.tunableProperties.length > 0 && this.menuContainer.nativeElement.offsetHeight / this.tunableProperties.length
const adjHeight = - this.focusedIndex * cellHeight - cellHeight * 0.5
const min = - cellHeight * 0.5
const max = - this.tunableProperties.length * cellHeight + cellHeight * 0.5
const finalYTranslate = clamp(adjHeight + deltaY, min, max )
this.menuTransform = `translate(0px, ${finalYTranslate}px)`
})
this.showProperties$.pipe(
takeUntil(this._onDestroySubject),
......@@ -139,47 +199,12 @@ export class MobileOverlay implements AfterViewInit, OnDestroy{
if(this.focusItemIndex >= 0){
this._focusedProperties = this.tunableProperties[this.focusItemIndex]
}
this.scrollHeight = 0
})
}
scrollHeight : number = 0
public menuTransform = `translate(0px, 0px)`
get defaultY(){
return this.tunableProperties.findIndex(p => p === this.focusedProperty) * this.menuCellHeight
}
get menuTransform(){
return `translate(0px, ${this.menuYTranslate}px)`
}
public focusItemIndex: number = 0
get menuYTranslate(){
return this.menuContainer
? Math.max(
Math.min(
this.defaultY + this.scrollHeight,
this.menuContainerHeight / 2 - this.menuCellHeight / 2
),
- this.menuContainerHeight / 2 + this.menuCellHeight / 2
)
: this.defaultY
}
get menuContainerHeight(){
return this.menuContainer
? this.menuContainer.nativeElement.offsetHeight
: 0
}
get menuCellHeight(){
return this.tunableProperties.length > 0
? this.menuContainerHeight / this.tunableProperties.length
: 0
}
get focusItemIndex():number{
return this.menuContainer
? Math.floor((this.menuContainerHeight / 2 - this.menuYTranslate) / this.menuCellHeight)
: -1
}
}
\ No newline at end of file
......@@ -18,24 +18,34 @@
left: 0;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
color : black;
background-color: rgba(255, 255, 255, 0.5);
}
:host-context([darktheme="true"]) [screen]
{
color : white;
background-color: rgba(0, 0, 0, 0.5);
}
[intersector]
{
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 50%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
[mobileMenuContainer]
{
z-index: 1000;
transform: translate(0, 155px);
}
.scrollFocus:after
......
<div screen *ngIf = "showProperties$ | async">
<div [style.transform] = "menuTransform" class = "btn-group-vertical" role = "group" mobileMenuContainer #mobileMenuContainer>
<div
*ngFor = "let p of tunableProperties; let i = index"
[ngClass] = "{'active' : p === focusedProperty, 'scrollFocus' : i === focusItemIndex}"
class = "btn btn-default theme-controlled">
<span>
{{ p }}
</span>
<div screen [hidden]="!(showProperties$ | async)">
<div intersector #intersector>
<div [style.transform] = "menuTransform" class = "btn-group-vertical" role = "group" mobileMenuContainer #mobileMenuContainer>
<div
*ngFor = "let p of tunableProperties; let i = index"
[ngClass] = "{'active' : p === focusedProperty, 'scrollFocus': i === focusItemIndex}"
class = "btn btn-default theme-controlled property scrollFocus">
<!-- scrollFocus class -->
<span>
{{ p }}
</span>
</div>
</div>
</div>
{{ menuTransform }} {{ focusItemIndex }}
</div>
<ng-content *ngIf = "showScreen$ | async" select = "[guide]" guide>
<ng-content *ngIf="showDelta$ | async" select="[delta]" guide>
</ng-content>
<div #initiator>
<ng-content *ngIf="showScreen$ | async" select="[guide]" guide>
</ng-content>
<div [hidden]="!(showInitiator$ | async)" #initiator>
<ng-content select="[initiator]">
</ng-content>
</div>
\ No newline at end of file
......@@ -577,53 +577,18 @@ export class NehubaContainer implements OnInit, OnDestroy{
public showObliqueRotate$ : Observable<boolean>
ngAfterViewInit(){
// if(this.isMobile){
// this.showObliqueScreen$ = merge(
// fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchstart'),
// fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchend')
// ).pipe(
// map((ev:TouchEvent) => ev.touches.length === 1)
// )
// fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchstart').pipe(
// switchMap(() => fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchmove').pipe(
// takeUntil(fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchend').pipe(
// filter((ev:TouchEvent) => ev.touches.length === 0)
// )),
// map((ev:TouchEvent) => (ev.preventDefault(), ev.stopPropagation(), ev)),
// filter((ev:TouchEvent) => ev.touches.length === 1),
// map((ev:TouchEvent) => [ev.touches[0].screenX, ev.touches[0].screenY] ),
// /* TODO reduce boiler plate, export */
// scan((acc,curr) => acc.length < 2
// ? acc.concat([curr])
// : acc.slice(1).concat([curr]), []),
// filter(isdouble => isdouble.length === 2),
// map(double => ({
// deltaX : double[1][0] - double[0][0],
// deltaY : double[1][1] - double[0][1]
// }))
// ))
// )
// .subscribe(({deltaX, deltaY}) => {
// })
// }
}
ngOnDestroy(){
this.subscriptions.forEach(s=>s.unsubscribe())
}
get tunableMobileProperties(){
return ['Oblique Rotate X', 'Oblique Rotate Y', 'Oblique Rotate Z']
}
public tunableMobileProperties = ['Oblique Rotate X', 'Oblique Rotate Y', 'Oblique Rotate Z']
public selectedProp = null
handleMobileOverlayEvent(obj:any){
const {delta, selectedProp} = obj
this.selectedProp = selectedProp
const idx = this.tunableMobileProperties.findIndex(p => p === selectedProp)
idx === 0
......
......@@ -142,13 +142,15 @@ div.loadingIndicator div.spinnerAnimationCircle
div[mobileObliqueCtrl]
{
font-size: 200%;
margin-top:-2.5rem;
margin-left:-2.5rem;
padding-left: 1rem;
padding-top: 1rem;
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: all;
}
......@@ -164,16 +166,39 @@ div[mobileObliqueScreen]
pointer-events: all;
}
div[mobileObliqueGuide]
div.base
{
position : absolute;
top: 40%;
width: 100%;
top: 50%;
left: 50%;
width: 0;
height: 0;
display:flex;
flex-direction: column;
flex-direction: column-reverse;
align-items: center;
}
div[delta]
{
white-space: nowrap
}
div[mobileObliqueGuide]
{
background-color: rgba(250,250,250,0.8);
}
div[mobileObliqueGuide] > *
{
white-space: nowrap;
}
:host-context([darktheme="true"]) div[mobileObliqueGuide]
{
background-color: rgba(50,50,50,0.8);
}
div#scratch-pad
{
position: absolute;
......@@ -182,4 +207,5 @@ div#scratch-pad
width: 100%;
height: 100%;
pointer-events: none;
}
\ No newline at end of file
}
......@@ -155,15 +155,22 @@
</div>
<mobile-overlay
*ngIf="isMobile && viewerLoaded"
*ngIf="isMobile && viewerLoaded"
[tunableProperties]="tunableMobileProperties"
(deltaValue)="handleMobileOverlayEvent($event)">
<div mobileObliqueGuide guide>
<div>
<i class="fas fa-resize-vertical"></i> oblique mode
<div class="base" delta>
<div mobileObliqueGuide class="p-2 mb-4 shadow">
{{ selectedProp }}
</div>
<div>
<i class="fas fa-resize-horizontal"></i> rotate slice
</div>
<div class="base" guide>
<div mobileObliqueGuide class="p-2 mb-4 shadow">
<div>
<i class="fas fa-arrows-alt-v"></i> oblique mode
</div>
<div>
<i class="fas fa-arrows-alt-h"></i> rotate slice
</div>
</div>
</div>
<div mobileObliqueCtrl initiator>
......
......@@ -11,7 +11,8 @@ import { map, filter, distinctUntilChanged } from "rxjs/operators";
selector: 'signin-banner',
templateUrl: './signinBanner.template.html',
styleUrls: [
'./signinBanner.style.css'
'./signinBanner.style.css',
'../btnShadow.style.css'
],
changeDetection: ChangeDetectionStrategy.OnPush
})
......
<dropdown-component
*ngIf="!isMobile"
(itemSelected)="changeTemplate($event)"
[activeDisplay]="displayActiveTemplate"
[selectedItem]="selectedTemplate$ | async"
......@@ -23,11 +24,12 @@
</div>
<!-- signin -->
<div
*ngIf="!isMobile"
(click)="showSignin()"
class="btn btn-outline-secondary btn-sm rounded-circle">
<i
[ngClass]="user ? 'fa-user' : 'fa-sign-in-alt'"
class="fas"></i>
</div>
<div class="btnWrapper">
<div
(click)="showSignin()"
class="btn btn-outline-secondary btn-sm rounded-circle">
<i
[ngClass]="user ? 'fa-user' : 'fa-sign-in-alt'"
class="fas"></i>
</div>
</div>
\ No newline at end of file
......@@ -12,4 +12,16 @@ export function* timedValues(ms:number = 500,mode:string = 'linear'){
yield getValue( (Date.now() - startTime) / ms )
}
return 1
}
export function clamp(val: number, min: number, max: number) {
const _min = min < max ? min : max
const _max = min < max ? max : min
return Math.min(
Math.max(
val,
_min
),
_max
)
}
\ No newline at end of file
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment