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",
"extract-text-webpack-plugin": "^3.0.2",
"frptools": "3.0.1",
"linear-partitioning": "0.3.2",
"pica": "~2.0.8",
"pouchdb-adapter-http": "~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 { Icon } from './components/icon.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) {
Array.from(files || evt.currentTarget.files).forEach(ImageType.upload);
@ -198,19 +198,25 @@ export function AllImagesView(vm, params, key, context) {
}
return function() {
return scrollView(
return allImagesContainer(
{
class: 'allImages',
onscroll: handleContentScroll
},
[
allImagesContent(
{
onclick: {
'.photoSelect .icon svg path': toggleSelect,
'.photoSelect .icon': toggleSelect,
'.sectionSelectButton .icon': toggleAll,
'.sectionSelectButton .icon svg path': toggleAll,
'.photoOverlay': photoClick
},
onscroll: handleContentScroll
}
},
sections().map(renderSection)
)
]
);
};
}
@ -229,6 +235,10 @@ const uploadButton = styled(
CLICKABLE
);
const scrollView = styled({
const allImagesContainer = styled(
{
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 boxShadowStyle = computed(
t => (t === 0 ? 'none' : `0px ${Math.min(t / 10, 3)}px 3px rgba(0, 0, 0, .2)`),
[companionScrollTop]
);
const boxShadowStyle = computed(t => (t === 0 ? 'none' : `0px 3px 3px rgba(0, 0, 0, .2)`), [
companionScrollTop
]);
const containerStyle = computed(
(boxShadow, style) => ({

View File

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

View File

@ -18,6 +18,7 @@ import { AppBarView } from './components/appbar.js';
import { Icon } from './components/icon.js';
import { routeChanged } from '../services/router.js';
import { injectStyle, styled } from '../services/style.js';
import { FILL_STYLE } from './styles.js';
export function GalleryView(vm) {
const context = {};
@ -31,10 +32,6 @@ export function GalleryView(vm) {
vm.redraw();
});
function handleContentScroll(evt) {
context.appbar.companionScrollTop(evt.target.scrollTop);
}
function renderMain() {
return [
iv(appbar),
@ -63,7 +60,7 @@ export function GalleryView(vm) {
}
return function render() {
return el('.gallery', { class: fill }, [
return container({ class: 'gallery' }, [
vw(
Dropzone,
{
@ -79,16 +76,19 @@ export function GalleryView(vm) {
};
}
const FILL_STYLE = {
display: 'flex',
flex: 1,
flexDirection: 'column'
};
const fill = injectStyle(FILL_STYLE);
const content = styled(
const container = styled(
{
['-webkit-transform']: 'translate3d(0,0,0);' // http://blog.getpostman.com/2015/01/23/ui-repaint-issue-on-chrome/
overflow: 'hidden'
},
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';
import { router } from '../services/router.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 { AttachmentImageView } from './components/attachmentImage.js';
@ -22,7 +22,7 @@ export function SectionPhoto(vm, { doc }) {
subscribeToRender(vm, [hover, hoverSelectButton]);
return function render(vm, { isSelected, selectMode }) {
return function render(vm, { isSelected, selectMode, width, height }) {
return photoContainer(
{
href,
@ -32,6 +32,10 @@ export function SectionPhoto(vm, { doc }) {
css: {
cursor: selectMode ? CLICKABLE.cursor : 'zoom-in'
},
style: {
width,
height
},
_data: doc
},
[
@ -39,6 +43,10 @@ export function SectionPhoto(vm, { doc }) {
src: doc.sizes.thumbnail || doc.sizes.full,
css: {
transform: isSelected ? 'translateZ(-50px)' : null
},
style: {
width,
height
}
}),
photoSelectButton(
@ -66,6 +74,10 @@ export function SectionPhoto(vm, { doc }) {
css: {
transform: isSelected ? 'translateZ(-50px)' : null,
opacity: selectMode || hover() ? 0.7 : 0
},
style: {
width,
height
}
})
]
@ -78,10 +90,11 @@ const photoContainer = styled('a', {
perspective: '1000px',
backgroundColor: '#eee',
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',
top: 0,
left: 0,
@ -100,7 +113,7 @@ const photoSelectButton = styled(DEFAULT_TRANSITION, CLICKABLE, {
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
top: '0px',
left: '0px',

View File

@ -1,36 +1,88 @@
import partition from 'linear-partitioning';
import {
defineView as vw,
defineElement as el,
patchRefStyle,
patchNodeStyle
patchNodeStyle,
subscribeToRender,
viewportSize
} from '../utils/domvm.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 { SectionPhoto } from './sectionPhoto.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) {
const { appbar } = context;
const { title, photos } = params;
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) {
const { selectedIds, selectMode } = params;
function photoTemplate(doc) {
function photoTemplate({ photo, width, height }) {
return vw(
SectionPhoto,
{
doc,
isSelected: selectedIds.has(doc._id),
selectMode
doc: photo,
isSelected: selectedIds.has(photo._id),
selectMode,
width,
height
},
doc._hash(),
photo._hash(),
context
);
}
function sectionRowTemplate(photos) {
return sectionRow(photos.map(photoTemplate));
}
return sectionContainer(
{
class: 'section',
@ -57,14 +109,15 @@ export function SectionView(vm, params, key, context) {
[Icon({ name: 'check_circle', size: 0.25 })]
)
]),
sectionContent(photos.map(photoTemplate))
sectionContent(calculateSections(photos).map(sectionRowTemplate))
]
);
};
}
const sectionContainer = styled({
margin: '10px'
margin: `${CONTENT_MARGIN}px`,
flexDirection: 'column'
});
const sectionTitle = styled({
@ -75,6 +128,12 @@ const sectionTitle = styled({
const sectionContent = styled({
display: 'flex',
alignItems: 'flex-start',
flexDirection: 'column'
});
const sectionRow = styled({
flexDirection: 'row',
flex: 1,
userSelect: 'none'
});

View File

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

View File

@ -1,7 +1,7 @@
// export * from 'domvm/dist/dev/domvm.dev.js';
export * 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 { error } from '../services/console.js';
@ -50,3 +50,15 @@ export function renderSwitch(renderMap, switchValue) {
const params = renderMap[switchValue];
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();