Skip to content

Commit

Permalink
Add settings menu and allow customization of print options
Browse files Browse the repository at this point in the history
  • Loading branch information
MacRusher committed Nov 1, 2021
1 parent 4bc3ec5 commit 9a9bfc0
Show file tree
Hide file tree
Showing 9 changed files with 1,008 additions and 106 deletions.
941 changes: 852 additions & 89 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
}
},
"dependencies": {
"ajv": "8.6.3",
"jimp": "0.16.1",
"jspdf": "2.3.1",
"lodash": "4.17.21",
Expand All @@ -34,7 +35,10 @@
"redux-logic": "3.0.3",
"rxjs": "7.3.0",
"semantic-ui-css": "2.4.1",
"semantic-ui-react": "2.0.3"
"semantic-ui-react": "2.0.3",
"uniforms": "3.6.2",
"uniforms-bridge-json-schema": "3.6.2",
"uniforms-semantic": "3.6.2"
},
"devDependencies": {
"@types/jest": "27.0.2",
Expand Down
8 changes: 8 additions & 0 deletions src/api/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import {
REMOVE_IMAGE,
RemoveAllAction,
RemoveImageAction,
SET_SETTINGS,
SetSettingsAction,
Settings,
UPLOAD_IMAGES,
UploadImagesAction,
} from './types';
Expand Down Expand Up @@ -48,3 +51,8 @@ export const uploadImages = (files: FileList | null): UploadImagesAction => ({
type: UPLOAD_IMAGES,
payload: files ? [...files] : [],
});

export const setSettings = (settings: Partial<Settings>): SetSettingsAction => ({
type: SET_SETTINGS,
payload: settings,
});
39 changes: 30 additions & 9 deletions src/api/lib.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import Ajv, { SchemaObject } from 'ajv';
import Jimp from 'jimp';
import JsPDF from 'jspdf';
import chunk from 'lodash/chunk';
import random from 'lodash/random';
import shuffle from 'lodash/shuffle';
import { JSONSchemaBridge } from 'uniforms-bridge-json-schema';

import { CardImage, CardSymbol, Prime } from './types';
import { CardImage, CardSymbol, Prime, Settings } from './types';

/**
* Generate supported plains (dimensions) according to the Ray-Chaudhuri–Wilson theorem
Expand Down Expand Up @@ -67,23 +69,26 @@ export const sleep = (t: number = 0) => new Promise(r => setTimeout(r, t));
*/
export const generatePdf = async (
images: CardImage[] = [],
options: { n: Prime },
options: { n: Prime } & Settings,
): Promise<JsPDF> => {
const { n } = options;
const {
n, // No of plains
pageWidth = 210, // A4
pageHeight = 297, // A4
cardRadius = 42, // Size of a single card
symbolMargin = -0.1, // Percent of card radius; value is negative to allow overlap since rotated symbols are smaller
rotateSymbols = true, // Whether the symbols should be randomly rotated
} = options;

// Apply images to generated card sequences
const cards = generateCards(n).map(card => card.map(s => images[s]));

// PDF sizes in mm
const pageWidth = 210; // A4
const pageHeight = 297; // A4
const cardRadius = 42; // Size of a single card
const columnsPerPage = Math.floor(pageWidth / (cardRadius * 2));
const rowsPerPage = Math.floor(pageHeight / (cardRadius * 2));
const cardsPerPage = columnsPerPage * rowsPerPage;
const columnWidth = pageWidth / columnsPerPage;
const rowHeight = pageHeight / rowsPerPage;
const symbolMargin = -0.1; // Percent of card radius; value is negative to allow overlap since rotated symbols are smaller

const pdf = new JsPDF();

Expand All @@ -101,7 +106,9 @@ export const generatePdf = async (

// Add symbols to pdf
for (let s of symbols) {
s = await rotateSymbol(s);
if (rotateSymbols) {
s = await rotateSymbol(s);
}
pdf.addImage(
s.image.base64src,
'PNG',
Expand All @@ -111,7 +118,7 @@ export const generatePdf = async (
cardRadius * s.height,
s.image.id,
'NONE',
0
0,
)
}
}
Expand Down Expand Up @@ -223,3 +230,17 @@ function getCardMiddle(i: number, columnWidth: number, rowHeight: number): { x:
}
}

export function createBridge(schema: SchemaObject): JSONSchemaBridge {
const ajv = new Ajv({ allErrors: true, useDefaults: true });

function createValidator(schema: SchemaObject) {
const validator = ajv.compile(schema);

return (model: object) => {
validator(model);
return validator.errors?.length ? { details: validator.errors } : null;
};
}

return new JSONSchemaBridge(schema, createValidator(schema));
}
4 changes: 2 additions & 2 deletions src/api/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ export const generatePdfLogic = createLogic({
dispatch,
done,
) {
const images: CardImage[] = getState().images;
const { settings, images } = getState();

// Unlock the thread before heavy computations starts
await sleep(100);

const pdf = await generatePdf(images, action.payload).catch(err =>
const pdf = await generatePdf(images, { ...settings, ...action.payload }).catch(err =>
alert(err.message),
);

Expand Down
16 changes: 16 additions & 0 deletions src/api/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@ import {
GENERATE_PDF_COMPLETE,
REMOVE_ALL,
REMOVE_IMAGE,
SET_SETTINGS,
State,
} from './types';

const initialState: State = {
images: [],
processing: false,
settings: {
pageWidth: 210, // A4
pageHeight: 297, // A4
cardRadius: 42, // Size of a single card
symbolMargin: -0.1, // Percent of card radius
rotateSymbols: true, // Whether the symbols should be randomly rotated
}
};

export default function(state = initialState, action: Actions): State {
Expand Down Expand Up @@ -40,6 +48,14 @@ export default function(state = initialState, action: Actions): State {
...state,
images: state.images.filter(image => image.id !== action.payload),
};
case SET_SETTINGS:
return {
...state,
settings: {
...state.settings,
...action.payload,
},
};
default:
return state;
}
Expand Down
17 changes: 16 additions & 1 deletion src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
export interface State {
images: CardImage[];
processing: boolean;
settings: Settings;
}

// Possible redux action types
Expand All @@ -12,10 +13,19 @@ export const LOAD_EXAMPLES = 'LOAD_EXAMPLES';
export const REMOVE_ALL = 'REMOVE_ALL';
export const REMOVE_IMAGE = 'REMOVE_IMAGE';
export const UPLOAD_IMAGES = 'UPLOAD_IMAGES';
export const SET_SETTINGS = 'SET_SETTINGS';

// Payload types
export type Prime = 2 | 3 | 5 | 7 | 11;

export interface Settings {
pageWidth: number; // Page width in mm
pageHeight: number; // Page height in mm
cardRadius: number; // Size of a single card
symbolMargin: number; // Percent of card radius
rotateSymbols: boolean; // Whether the symbols should be randomly rotated
}

export interface CardImage {
base64src: string;
id: string;
Expand Down Expand Up @@ -66,6 +76,10 @@ export interface UploadImagesAction {
type: typeof UPLOAD_IMAGES;
payload: File[];
}
export interface SetSettingsAction {
type: typeof SET_SETTINGS;
payload: Partial<Settings>;
}

export type Actions =
| AppendImagesAction
Expand All @@ -74,4 +88,5 @@ export type Actions =
| LoadExamplesAction
| RemoveAllAction
| RemoveImageAction
| UploadImagesAction;
| UploadImagesAction
| SetSettingsAction;
13 changes: 9 additions & 4 deletions src/components/Files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
import { State } from '../api/store';
import { CardImage } from '../api/types';

import Settings from './Settings';

interface Props {
images: CardImage[];
loadExamples: typeof loadExamples;
Expand Down Expand Up @@ -71,10 +73,13 @@ const Files: FC<Props> = ({
))}
</Image.Group>
{images.length > 0 && (
<Button onClick={removeAll}>
<Icon name="trash" />
Remove all images
</Button>
<>
<Button onClick={removeAll}>
<Icon name="trash" />
Remove all images
</Button>
<Settings />
</>
)}
</Segment>
</Container>
Expand Down
70 changes: 70 additions & 0 deletions src/components/Settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { FC, useState } from 'react';
import { connect } from 'react-redux';
import { Button, Icon, Modal, Message } from 'semantic-ui-react';
import { AutoForm } from 'uniforms-semantic';

import { setSettings } from '../api/actions';
import { createBridge } from '../api/lib';
import { State } from '../api/store';
import { Settings } from '../api/types';

const formSchema = createBridge({
title: 'Settings',
type: 'object',
properties: {
pageWidth: { type: 'integer' },
pageHeight: { type: 'integer' },
cardRadius: { type: 'number' },
symbolMargin: { type: 'number' },
rotateSymbols: { type: 'boolean' },
},
});

interface Props {
settings: Settings,
setSettings: typeof setSettings;
}

const SettingsComponent: FC<Props> = ({
settings,
setSettings,
}) => {
const [open, setOpen] = useState(false);

return (
<Modal
onClose={() => setOpen(false)}
onOpen={() => setOpen(true)}
open={open}
dimmer="blurring"
trigger={
<Button>
<Icon name="cog"/>
Settings
</Button>
}
>
<Modal.Header>Adjust settings for the print</Modal.Header>
<Modal.Content>
<AutoForm
schema={formSchema}
model={settings}
onSubmit={(model) => {
setSettings(model);
setOpen(false);
}}
/>
<Message success header="Tips:" list={[
'Page sizes and card radius are in millimeters',
'Symbol margin is a percentage of a symbol that should be left as a margin between other symbols',
'If you rotate symbols, the margin value should be negative to allow overlap since rotated symbols are smaller',
'Experiment and see what fit best for your pictures!'
]}/>
</Modal.Content>
</Modal>
);
};

export default connect((state: State) => ({ settings: state.settings }), {
setSettings,
})(SettingsComponent);

0 comments on commit 9a9bfc0

Please sign in to comment.