Introduce linear-partitioning into image layout
It's not perfect but a good start.
This commit is contained in:
parent
3c7363ec9f
commit
8be9ce010c
@ -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",
|
||||||
|
|||||||
@ -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',
|
||||||
|
onscroll: handleContentScroll
|
||||||
|
},
|
||||||
|
[
|
||||||
|
allImagesContent(
|
||||||
|
{
|
||||||
onclick: {
|
onclick: {
|
||||||
'.photoSelect .icon svg path': toggleSelect,
|
'.photoSelect .icon svg path': toggleSelect,
|
||||||
'.photoSelect .icon': toggleSelect,
|
'.photoSelect .icon': toggleSelect,
|
||||||
'.sectionSelectButton .icon': toggleAll,
|
'.sectionSelectButton .icon': toggleAll,
|
||||||
'.sectionSelectButton .icon svg path': toggleAll,
|
'.sectionSelectButton .icon svg path': toggleAll,
|
||||||
'.photoOverlay': photoClick
|
'.photoOverlay': photoClick
|
||||||
},
|
}
|
||||||
onscroll: handleContentScroll
|
|
||||||
},
|
},
|
||||||
sections().map(renderSection)
|
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({});
|
||||||
|
|||||||
@ -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) => ({
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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/
|
||||||
|
});
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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%'
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user