Introduce linear-partitioning into image layout

It's not perfect but a good start.
This commit is contained in:
Timothy Farrell 2018-01-01 22:09:23 -06:00
parent 3c7363ec9f
commit 8be9ce010c
9 changed files with 143 additions and 58 deletions

View File

@ -18,6 +18,7 @@
"exif-parser": "~0.1.9", "exif-parser": "~0.1.9",
"extract-text-webpack-plugin": "^3.0.2", "extract-text-webpack-plugin": "^3.0.2",
"frptools": "3.0.1", "frptools": "3.0.1",
"linear-partitioning": "0.3.2",
"pica": "~2.0.8", "pica": "~2.0.8",
"pouchdb-adapter-http": "~6.4.1", "pouchdb-adapter-http": "~6.4.1",
"pouchdb-adapter-idb": "~6.4.1", "pouchdb-adapter-idb": "~6.4.1",

View File

@ -15,7 +15,7 @@ import { pouchDocArrayHash, pouchDocHash, hashSet, extractID } from '../utils/co
import { SectionView } from './sectionView.js'; import { SectionView } from './sectionView.js';
import { Icon } from './components/icon.js'; import { Icon } from './components/icon.js';
import { injectStyle, styled } from '../services/style.js'; import { injectStyle, styled } from '../services/style.js';
import { CLICKABLE } from './styles.js'; import { CLICKABLE, FILL_STYLE } from './styles.js';
export function uploadImages(evt, files) { export function uploadImages(evt, files) {
Array.from(files || evt.currentTarget.files).forEach(ImageType.upload); Array.from(files || evt.currentTarget.files).forEach(ImageType.upload);
@ -198,19 +198,25 @@ export function AllImagesView(vm, params, key, context) {
} }
return function() { return function() {
return scrollView( return allImagesContainer(
{ {
class: 'allImages', class: 'allImages',
onclick: {
'.photoSelect .icon svg path': toggleSelect,
'.photoSelect .icon': toggleSelect,
'.sectionSelectButton .icon': toggleAll,
'.sectionSelectButton .icon svg path': toggleAll,
'.photoOverlay': photoClick
},
onscroll: handleContentScroll onscroll: handleContentScroll
}, },
sections().map(renderSection) [
allImagesContent(
{
onclick: {
'.photoSelect .icon svg path': toggleSelect,
'.photoSelect .icon': toggleSelect,
'.sectionSelectButton .icon': toggleAll,
'.sectionSelectButton .icon svg path': toggleAll,
'.photoOverlay': photoClick
}
},
sections().map(renderSection)
)
]
); );
}; };
} }
@ -229,6 +235,10 @@ const uploadButton = styled(
CLICKABLE CLICKABLE
); );
const scrollView = styled({ const allImagesContainer = styled(
overflow: 'scroll' {
}); overflow: 'scroll'
},
FILL_STYLE
);
const allImagesContent = styled({});

View File

@ -23,10 +23,9 @@ export function AppBarView(vm, params, key, opts) {
); );
const stateStyle = computed(pick('style', {}), [currentState]); const stateStyle = computed(pick('style', {}), [currentState]);
const boxShadowStyle = computed( const boxShadowStyle = computed(t => (t === 0 ? 'none' : `0px 3px 3px rgba(0, 0, 0, .2)`), [
t => (t === 0 ? 'none' : `0px ${Math.min(t / 10, 3)}px 3px rgba(0, 0, 0, .2)`), companionScrollTop
[companionScrollTop] ]);
);
const containerStyle = computed( const containerStyle = computed(
(boxShadow, style) => ({ (boxShadow, style) => ({

View File

@ -4,6 +4,7 @@ import {
subscribeToRender, subscribeToRender,
defineView, defineView,
nodeParentWithType, nodeParentWithType,
viewportSize,
defineElement as el defineElement as el
} from '../utils/domvm.js'; } from '../utils/domvm.js';
@ -21,13 +22,9 @@ export function FocusView(vm, params, key, { appbar }) {
const id = prop(); const id = prop();
const doc = prop(null, pouchDocHash); const doc = prop(null, pouchDocHash);
const { body } = document; const { body } = document;
const windowSize = prop({}, o => (o ? `${o.width}x${o.height}` : ''));
const nextLink = prop(); const nextLink = prop();
const prevLink = prop(); const prevLink = prop();
const extractWindowSize = () =>
windowSize({ width: window.innerWidth, height: window.innerHeight });
const imageStyle = computed( const imageStyle = computed(
({ width: iw, height: ih }, { width: vw, height: vh }) => { ({ width: iw, height: ih }, { width: vw, height: vh }) => {
const imageRatio = iw / ih; const imageRatio = iw / ih;
@ -45,7 +42,7 @@ export function FocusView(vm, params, key, { appbar }) {
}; };
} }
}, },
[doc, windowSize] [doc, viewportSize]
); );
function navBack() { function navBack() {
@ -83,18 +80,11 @@ export function FocusView(vm, params, key, { appbar }) {
} }
}); });
// Prime our window size
extractWindowSize();
window.addEventListener('resize', extractWindowSize);
// Subscribe to our changables. // Subscribe to our changables.
subscribeToRender( subscribeToRender(
vm, vm,
[doc, imageStyle, nextLink, prevLink], [doc, imageStyle, nextLink, prevLink],
[ [
// Keep up with the window resizing
() => window.removeEventListener('resize', extractWindowSize),
// Look for our image and set it. // Look for our image and set it.
id.subscribe(async _id => { id.subscribe(async _id => {
if (!_id) { if (!_id) {

View File

@ -18,6 +18,7 @@ import { AppBarView } from './components/appbar.js';
import { Icon } from './components/icon.js'; import { Icon } from './components/icon.js';
import { routeChanged } from '../services/router.js'; import { routeChanged } from '../services/router.js';
import { injectStyle, styled } from '../services/style.js'; import { injectStyle, styled } from '../services/style.js';
import { FILL_STYLE } from './styles.js';
export function GalleryView(vm) { export function GalleryView(vm) {
const context = {}; const context = {};
@ -31,10 +32,6 @@ export function GalleryView(vm) {
vm.redraw(); vm.redraw();
}); });
function handleContentScroll(evt) {
context.appbar.companionScrollTop(evt.target.scrollTop);
}
function renderMain() { function renderMain() {
return [ return [
iv(appbar), iv(appbar),
@ -63,7 +60,7 @@ export function GalleryView(vm) {
} }
return function render() { return function render() {
return el('.gallery', { class: fill }, [ return container({ class: 'gallery' }, [
vw( vw(
Dropzone, Dropzone,
{ {
@ -79,16 +76,19 @@ export function GalleryView(vm) {
}; };
} }
const FILL_STYLE = {
display: 'flex',
flex: 1,
flexDirection: 'column'
};
const fill = injectStyle(FILL_STYLE); const fill = injectStyle(FILL_STYLE);
const container = styled(
const content = styled(
{ {
['-webkit-transform']: 'translate3d(0,0,0);' // http://blog.getpostman.com/2015/01/23/ui-repaint-issue-on-chrome/ overflow: 'hidden'
}, },
FILL_STYLE FILL_STYLE
); );
const content = styled({
position: 'absolute',
top: '58px',
bottom: 0,
left: 0,
right: 0,
['-webkit-transform']: 'translate3d(0,0,0);' // http://blog.getpostman.com/2015/01/23/ui-repaint-issue-on-chrome/
});

View File

@ -9,7 +9,7 @@ import {
} from '../utils/domvm.js'; } from '../utils/domvm.js';
import { router } from '../services/router.js'; import { router } from '../services/router.js';
import { injectStyle, styled } from '../services/style.js'; import { injectStyle, styled } from '../services/style.js';
import { DEFAULT_TRANSITION, CSS_FULL_SIZE, IMAGE_MARGIN, CLICKABLE } from './styles.js'; import { DEFAULT_TRANSITION, FILL_STYLE, IMAGE_MARGIN, CLICKABLE } from './styles.js';
import { Icon } from './components/icon.js'; import { Icon } from './components/icon.js';
import { AttachmentImageView } from './components/attachmentImage.js'; import { AttachmentImageView } from './components/attachmentImage.js';
@ -22,7 +22,7 @@ export function SectionPhoto(vm, { doc }) {
subscribeToRender(vm, [hover, hoverSelectButton]); subscribeToRender(vm, [hover, hoverSelectButton]);
return function render(vm, { isSelected, selectMode }) { return function render(vm, { isSelected, selectMode, width, height }) {
return photoContainer( return photoContainer(
{ {
href, href,
@ -32,6 +32,10 @@ export function SectionPhoto(vm, { doc }) {
css: { css: {
cursor: selectMode ? CLICKABLE.cursor : 'zoom-in' cursor: selectMode ? CLICKABLE.cursor : 'zoom-in'
}, },
style: {
width,
height
},
_data: doc _data: doc
}, },
[ [
@ -39,6 +43,10 @@ export function SectionPhoto(vm, { doc }) {
src: doc.sizes.thumbnail || doc.sizes.full, src: doc.sizes.thumbnail || doc.sizes.full,
css: { css: {
transform: isSelected ? 'translateZ(-50px)' : null transform: isSelected ? 'translateZ(-50px)' : null
},
style: {
width,
height
} }
}), }),
photoSelectButton( photoSelectButton(
@ -66,6 +74,10 @@ export function SectionPhoto(vm, { doc }) {
css: { css: {
transform: isSelected ? 'translateZ(-50px)' : null, transform: isSelected ? 'translateZ(-50px)' : null,
opacity: selectMode || hover() ? 0.7 : 0 opacity: selectMode || hover() ? 0.7 : 0
},
style: {
width,
height
} }
}) })
] ]
@ -78,10 +90,11 @@ const photoContainer = styled('a', {
perspective: '1000px', perspective: '1000px',
backgroundColor: '#eee', backgroundColor: '#eee',
margin: `${IMAGE_MARGIN}px`, margin: `${IMAGE_MARGIN}px`,
cursor: 'zoom-in' cursor: 'zoom-in',
display: 'inline-block'
}); });
const image = styled('img', CSS_FULL_SIZE, DEFAULT_TRANSITION, { const image = styled('img', FILL_STYLE, DEFAULT_TRANSITION, {
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
@ -100,7 +113,7 @@ const photoSelectButton = styled(DEFAULT_TRANSITION, CLICKABLE, {
opacity: 0 opacity: 0
}); });
const photoOverlay = styled(CSS_FULL_SIZE, DEFAULT_TRANSITION, { const photoOverlay = styled(FILL_STYLE, DEFAULT_TRANSITION, {
position: 'absolute', // Unnecessary but helps with a rendering bug in Chrome. https://gitlab.com/explorigin/gallery/issues/1 position: 'absolute', // Unnecessary but helps with a rendering bug in Chrome. https://gitlab.com/explorigin/gallery/issues/1
top: '0px', top: '0px',
left: '0px', left: '0px',

View File

@ -1,36 +1,88 @@
import partition from 'linear-partitioning';
import { import {
defineView as vw, defineView as vw,
defineElement as el, defineElement as el,
patchRefStyle, patchRefStyle,
patchNodeStyle patchNodeStyle,
subscribeToRender,
viewportSize
} from '../utils/domvm.js'; } from '../utils/domvm.js';
import { injectStyle, styled } from '../services/style.js'; import { injectStyle, styled } from '../services/style.js';
import { DEFAULT_TRANSITION, CLICKABLE } from './styles.js'; import { DEFAULT_TRANSITION, CLICKABLE, IMAGE_MARGIN, CONTENT_MARGIN } from './styles.js';
import { Icon } from './components/icon.js'; import { Icon } from './components/icon.js';
import { SectionPhoto } from './sectionPhoto.js'; import { SectionPhoto } from './sectionPhoto.js';
import { extractID } from '../utils/conversion.js'; import { extractID } from '../utils/conversion.js';
const OPTIMAL_IMAGE_HEIGHT = 140;
const ROW_HEIGHT_CUTOFF_MODIFIER = 2;
const IMAGE_MARGIN_WIDTH = 2 * IMAGE_MARGIN;
const aspectRatio = (img, margin = 0) => (img.width + margin) / (img.height + margin);
export function SectionView(vm, params, key, context) { export function SectionView(vm, params, key, context) {
const { appbar } = context; const { appbar } = context;
const { title, photos } = params; const { title, photos } = params;
const sectionSelectButtonRef = `secSel${key}`; const sectionSelectButtonRef = `secSel${key}`;
function calculateSections(photos) {
const { width: vw } = viewportSize();
const availableWidth = vw - CONTENT_MARGIN;
const totalImageRatio = photos.reduce(
(acc, img) => acc + aspectRatio(img, IMAGE_MARGIN_WIDTH),
0
);
const rowCount = Math.ceil(totalImageRatio * OPTIMAL_IMAGE_HEIGHT / availableWidth);
const rowRatios = partition(photos.map(aspectRatio), rowCount);
let index = 0;
const result = rowRatios.map(row => {
const rowTotal = row.reduce((acc, r) => acc + r, 0);
const imageRatio = row[0];
const portion = imageRatio / rowTotal;
let rowHeight = availableWidth * portion / aspectRatio(photos[index]);
if (rowHeight > OPTIMAL_IMAGE_HEIGHT * ROW_HEIGHT_CUTOFF_MODIFIER) {
rowHeight = OPTIMAL_IMAGE_HEIGHT * ROW_HEIGHT_CUTOFF_MODIFIER;
}
const rowResult = row.map((imageRatio, imgIndex) => ({
photo: photos[imgIndex + index],
width: imageRatio * rowHeight - IMAGE_MARGIN_WIDTH,
height: rowHeight
}));
index += row.length;
return rowResult;
});
return result;
}
subscribeToRender(vm, [viewportSize]);
return function render(vm, params) { return function render(vm, params) {
const { selectedIds, selectMode } = params; const { selectedIds, selectMode } = params;
function photoTemplate(doc) { function photoTemplate({ photo, width, height }) {
return vw( return vw(
SectionPhoto, SectionPhoto,
{ {
doc, doc: photo,
isSelected: selectedIds.has(doc._id), isSelected: selectedIds.has(photo._id),
selectMode selectMode,
width,
height
}, },
doc._hash(), photo._hash(),
context context
); );
} }
function sectionRowTemplate(photos) {
return sectionRow(photos.map(photoTemplate));
}
return sectionContainer( return sectionContainer(
{ {
class: 'section', class: 'section',
@ -57,14 +109,15 @@ export function SectionView(vm, params, key, context) {
[Icon({ name: 'check_circle', size: 0.25 })] [Icon({ name: 'check_circle', size: 0.25 })]
) )
]), ]),
sectionContent(photos.map(photoTemplate)) sectionContent(calculateSections(photos).map(sectionRowTemplate))
] ]
); );
}; };
} }
const sectionContainer = styled({ const sectionContainer = styled({
margin: '10px' margin: `${CONTENT_MARGIN}px`,
flexDirection: 'column'
}); });
const sectionTitle = styled({ const sectionTitle = styled({
@ -75,6 +128,12 @@ const sectionTitle = styled({
const sectionContent = styled({ const sectionContent = styled({
display: 'flex', display: 'flex',
alignItems: 'flex-start', alignItems: 'flex-start',
flexDirection: 'column'
});
const sectionRow = styled({
flexDirection: 'row',
flex: 1,
userSelect: 'none' userSelect: 'none'
}); });

View File

@ -1,6 +1,7 @@
export const IMAGE_MARGIN = 2; export const IMAGE_MARGIN = 2;
export const CONTENT_MARGIN = 10;
export const CSS_FULL_SIZE = { export const FILL_STYLE = {
width: '100%', width: '100%',
height: '100%' height: '100%'
}; };

View File

@ -1,7 +1,7 @@
// export * from 'domvm/dist/dev/domvm.dev.js'; // export * from 'domvm/dist/dev/domvm.dev.js';
export * from 'domvm/dist/mini/domvm.mini.js'; export * from 'domvm/dist/mini/domvm.mini.js';
import { defineView } from 'domvm/dist/mini/domvm.mini.js'; import { defineView } from 'domvm/dist/mini/domvm.mini.js';
import { call } from 'frptools'; import { prop, call } from 'frptools';
import { deepAssign } from './conversion.js'; import { deepAssign } from './conversion.js';
import { error } from '../services/console.js'; import { error } from '../services/console.js';
@ -50,3 +50,15 @@ export function renderSwitch(renderMap, switchValue) {
const params = renderMap[switchValue]; const params = renderMap[switchValue];
return params ? defineView.apply(null, params) : `VIEW ${switchValue} NOT FOUND`; return params ? defineView.apply(null, params) : `VIEW ${switchValue} NOT FOUND`;
} }
// Expose viewport size in a subscribable.
const SCROLLBAR_SIZE = 20;
export const viewportSize = prop({}, o => (o ? `${o.width}x${o.height}` : ''));
const extractWindowSize = () =>
viewportSize({
width: window.innerWidth - SCROLLBAR_SIZE,
height: window.innerHeight - SCROLLBAR_SIZE
});
window.addEventListener('resize', extractWindowSize);
// Prime our window size
extractWindowSize();