Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: TakenAt #244

Merged
merged 1 commit into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.4.69",
"version": "0.4.70",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
Binary file added web/public/fonts/poxel.ttf
Binary file not shown.
2 changes: 2 additions & 0 deletions web/src/core/exif-metadata/exif-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class ExifMetadata {
public iso: string | undefined;
public exposureTime: string | undefined;
public thumbnail: string | undefined;
public takenAt: string | undefined;

constructor(metadata: Tags) {
console.log(metadata);
Expand All @@ -28,6 +29,7 @@ class ExifMetadata {
this.iso = metadata?.ISOSpeedRatings?.value ? 'ISO' + metadata?.ISOSpeedRatings?.value?.toString() : undefined;
this.exposureTime = metadata?.ExposureTime?.description ? metadata?.ExposureTime?.description + 's' : undefined;
this.thumbnail = metadata?.Thumbnail?.base64 ? 'data:image/jpg;base64,' + metadata?.Thumbnail?.base64 : undefined;
this.takenAt = metadata?.DateCreated?.description;
}
}

Expand Down
8 changes: 8 additions & 0 deletions web/src/core/photo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ class Photo {
public get exposureTime(): string {
return this.metadata.exposureTime || '';
}

/**
* Returns the date the photo was taken.
* @example '2021-01-01T00:00:00.000+09:00'
*/
public get takenAt(): string {
return this.metadata.takenAt || '';
}
}

export default Photo;
1 change: 1 addition & 0 deletions web/src/fonts/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
enum Font {
Digital7 = 'digital-7',
Poxel = 'poxel',
DINAlternateBold = 'din-alternate-bold',
}

Expand Down
1 change: 1 addition & 0 deletions web/src/locales/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"aperture": "Aperture",
"shutter": "Shutter Speed",
"iso": "ISO",
"taken-at": "Taken At",

"root.tab.convert": "Convert",
"root.tab.theme-settings": "Theme Settings",
Expand Down
1 change: 1 addition & 0 deletions web/src/locales/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"aperture": "絞り値",
"shutter": "シャッタースピード",
"iso": "ISO感度",
"taken-at": "撮影日時",

"root.tab.convert": "変換",
"root.tab.theme-settings": "テーマ設定",
Expand Down
1 change: 1 addition & 0 deletions web/src/locales/translations/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"aperture": "조리개",
"shutter": "셔터 속도",
"iso": "ISO",
"taken-at": "촬영 일시",

"root.tab.convert": "프레임 씌우기",
"root.tab.theme-settings": "테마 설정",
Expand Down
3 changes: 3 additions & 0 deletions web/src/pages/convert/components/override-metadata.popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const OverrideMetadataPopup = () => {
const [aperture, setAperture] = useState(overrideMetadataTarget?.metadata.fNumber || '');
const [iso, setIso] = useState(overrideMetadataTarget?.metadata.iso || '');
const [shutter, setShutter] = useState(overrideMetadataTarget?.metadata.exposureTime || '');
const [takenAt, setTakenAt] = useState(overrideMetadataTarget?.metadata.takenAt || '');

useEffect(() => {
if (overrideMetadataTarget) setMake(overrideMetadataTarget.metadata.make || '');
Expand All @@ -25,6 +26,7 @@ const OverrideMetadataPopup = () => {
if (overrideMetadataTarget) setAperture(overrideMetadataTarget.metadata.fNumber || '');
if (overrideMetadataTarget) setIso(overrideMetadataTarget.metadata.iso || '');
if (overrideMetadataTarget) setShutter(overrideMetadataTarget.metadata.exposureTime || '');
if (overrideMetadataTarget) setTakenAt(overrideMetadataTarget.metadata.takenAt || '');
}, [overrideMetadataTarget]);

return (
Expand Down Expand Up @@ -52,6 +54,7 @@ const OverrideMetadataPopup = () => {
<ListInput label={t('aperture')} type="text" value={aperture} onChange={(e) => (setAperture(e.target.value), (overrideMetadataTarget!.metadata.fNumber = e.target.value))} />
<ListInput label={t('iso')} type="text" value={iso} onChange={(e) => (setIso(e.target.value), (overrideMetadataTarget!.metadata.iso = e.target.value))} />
<ListInput label={t('shutter')} type="text" value={shutter} onChange={(e) => (setShutter(e.target.value), (overrideMetadataTarget!.metadata.exposureTime = e.target.value))} />
<ListInput label={t('taken-at')} type="text" value={takenAt} onChange={(e) => (setTakenAt(e.target.value), (overrideMetadataTarget!.metadata.takenAt = e.target.value))} />
</List>
</Page>
</Popup>
Expand Down
4 changes: 3 additions & 1 deletion web/src/themes/03_ONE_LINE/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const ONE_LINE_OPTIONS: ThemeOption[] = [
{ id: 'FONT_SIZE', type: 'number', default: 70, description: 'px' },
{ id: 'FONT_FAMILY', type: 'select', options: ['Barlow', ...Object.values(Font)], default: 'Barlow', description: 'ex. din-alternate-bold, digital-7, Barlow, Arial, sans-serif' },
{ id: 'TOP_LABEL', type: 'string', default: '', description: 'ex. @username' },
{ id: 'DIVIDER', type: 'string', default: '|', description: 'ex. ∙' },
];

const ONE_LINE_FUNC: ThemeFunc = (photo: Photo, input: ThemeOptionInput, store: Store) => {
Expand All @@ -35,6 +36,7 @@ const ONE_LINE_FUNC: ThemeFunc = (photo: Photo, input: ThemeOptionInput, store:
const FONT_SIZE = input.get('FONT_SIZE') as number;
const FONT_FAMILY = (input.get('FONT_FAMILY') as string).trim();
const TOP_LABEL = (input.get('TOP_LABEL') as string).trim();
const DIVIDER = (input.get('DIVIDER') as string).trim();

const canvas = sandbox(photo, {
targetRatio: store.ratio,
Expand All @@ -56,7 +58,7 @@ const ONE_LINE_FUNC: ThemeFunc = (photo: Photo, input: ThemeOptionInput, store:
[photo.make, photo.model, photo.lensModel, ...(store.disableExposureMeter ? [] : [`${photo.iso}`, `${photo.focalLength}`, `${photo.fNumber}`, `${photo.exposureTime}`])]
.filter(Boolean)
.map((value) => value!.trim())
.join(' | '),
.join(` ${DIVIDER} `),
TEXT_ALIGN === 'left' ? PADDING_LEFT : TEXT_ALIGN === 'center' ? canvas.width / 2 : canvas.width - PADDING_RIGHT,
canvas.height - PADDING_BOTTOM / 2
);
Expand Down
6 changes: 4 additions & 2 deletions web/src/themes/04_TWO_LINE/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const TWO_LINE_OPTIONS: ThemeOption[] = [
{ id: 'FONT_SIZE', type: 'number', default: 70, description: 'px' },
{ id: 'FONT_FAMILY', type: 'select', options: ['Barlow', ...Object.values(Font)], default: 'Barlow', description: 'ex. din-alternate-bold, digital-7, Barlow, Arial, sans-serif' },
{ id: 'TOP_LABEL', type: 'string', default: '', description: 'ex. @username' },
{ id: 'DIVIDER', type: 'string', default: '|', description: 'ex. ∙' },
];

const TWO_LINE_FUNC: ThemeFunc = (photo: Photo, input: ThemeOptionInput, store: Store) => {
Expand All @@ -35,6 +36,7 @@ const TWO_LINE_FUNC: ThemeFunc = (photo: Photo, input: ThemeOptionInput, store:
const FONT_SIZE = input.get('FONT_SIZE') as number;
const FONT_FAMILY = (input.get('FONT_FAMILY') as string).trim();
const TOP_LABEL = (input.get('TOP_LABEL') as string).trim();
const DIVIDER = (input.get('DIVIDER') as string).trim();

const canvas = sandbox(photo, {
targetRatio: store.ratio,
Expand All @@ -55,14 +57,14 @@ const TWO_LINE_FUNC: ThemeFunc = (photo: Photo, input: ThemeOptionInput, store:
[photo.make, photo.model, photo.lensModel]
.filter(Boolean)
.map((value) => value!.trim())
.join(' | '),
.join(` ${DIVIDER} `),
TEXT_ALIGN === 'left' ? PADDING_LEFT : TEXT_ALIGN === 'center' ? canvas.width / 2 : canvas.width - PADDING_RIGHT,
canvas.height - PADDING_BOTTOM / 2 - FONT_SIZE / 1.5
);

if (!store.disableExposureMeter) {
context.fillText(
[`${photo.iso}`, `${photo.focalLength}`, `${photo.fNumber}`, `${photo.exposureTime}`].filter(Boolean).join(' | '),
[`${photo.iso}`, `${photo.focalLength}`, `${photo.fNumber}`, `${photo.exposureTime}`].filter(Boolean).join(` ${DIVIDER} `),
TEXT_ALIGN === 'left' ? PADDING_LEFT : TEXT_ALIGN === 'center' ? canvas.width / 2 : canvas.width - PADDING_RIGHT,
canvas.height - PADDING_BOTTOM / 2 + FONT_SIZE / 1.5
);
Expand Down
24 changes: 20 additions & 4 deletions web/src/themes/07_STRAP/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ supportLogo.set('SONY_LIGHT', loadLogo('/maker/light/sony.png'));
supportLogo.set('SONY_DARK', loadLogo('/maker/dark/sony.png'));

const STRAP_OPTIONS: ThemeOption[] = [
{ id: 'ARTIST', type: 'string', default: 'Your Name', description: 'your name' },
{ id: 'ARTIST', type: 'string', default: '', description: 'your name' },
{ id: 'DARK_MODE', type: 'boolean', default: false, description: 'enable to use dark mode' },
{ id: 'SECONDARY_TEXT_FONT_WEIGHT', type: 'range-slider', min: 100, max: 900, step: 100, default: 300, description: '100 - 900' },
{ id: 'PADDING_TOP', type: 'number', default: 0, description: 'px' },
Expand Down Expand Up @@ -96,9 +96,25 @@ const STRAP_FUNC: ThemeFunc = (photo: Photo, input: ThemeOptionInput, store: Sto
}

// Shot by
context.font = `normal ${SECONDARY_TEXT_FONT_WEIGHT} ${FONT_SIZE}px Barlow`;
context.fillStyle = SECONDARY_TEXT_COLOR;
context.fillText(`Shot by © ${ARTIST}`, FONT_SIZE, canvas.height - PADDING_BOTTOM / 2 + FONT_SIZE / 2);
if (ARTIST) {
context.font = `normal ${SECONDARY_TEXT_FONT_WEIGHT} ${FONT_SIZE}px Barlow`;
context.fillStyle = SECONDARY_TEXT_COLOR;
context.fillText(`Shot by © ${ARTIST}`, FONT_SIZE, canvas.height - PADDING_BOTTOM / 2 + FONT_SIZE / 2);
} else {
if (photo.takenAt) {
context.font = `normal ${SECONDARY_TEXT_FONT_WEIGHT} ${FONT_SIZE}px Barlow`;
context.fillStyle = SECONDARY_TEXT_COLOR;
const takenAt = new Date(photo.takenAt);
context.fillText(
`${takenAt.getFullYear()}年${(takenAt.getMonth() + 1).toString().padStart(2, '0')}月${takenAt.getDate().toString().padStart(2, '0')}日 ${takenAt
.getHours()
.toString()
.padStart(2, '0')}:${takenAt.getMinutes().toString().padStart(2, '0')}:${takenAt.getSeconds().toString().padStart(2, '0')}`,
FONT_SIZE,
canvas.height - PADDING_BOTTOM / 2 + FONT_SIZE / 2
);
}
}

// RIGHT SECOND
context.textAlign = 'right';
Expand Down
25 changes: 20 additions & 5 deletions web/src/themes/08_FILM/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { Store } from '../../store';
import sandbox from '../../core/drawing/sandbox';
import { ThemeFunc } from '../../core/drawing/theme';
import { ThemeOption, ThemeOptionInput } from '../../pages/theme/types/theme-option';
import Font from '../../fonts';

const FILM_OPTIONS: ThemeOption[] = [
{ id: 'FONT_FAMILY', type: 'select', options: ['Barlow', ...Object.values(Font)], default: 'digital-7', description: 'ex. din-alternate-bold, digital-7, Barlow, Arial, sans-serif' },
{ id: 'TEXT_COLOR', type: 'color', default: '#FFA500', description: 'default is orange hex code' },
{ id: 'BACKGROUND_COLOR', type: 'color', default: '#000000', description: '#ffffff is white, #000000 is black' },
{ id: 'PADDING_TOP', type: 'number', default: 0, description: 'px' },
Expand All @@ -14,6 +16,7 @@ const FILM_OPTIONS: ThemeOption[] = [
];

const FILM_FUNC: ThemeFunc = (photo: Photo, input: ThemeOptionInput, store: Store) => {
const FONT_FAMILY = (input.get('FONT_FAMILY') as string).trim();
const TEXT_COLOR = input.get('TEXT_COLOR') as string;
const BACKGROUND_COLOR = (input.get('BACKGROUND_COLOR') as string).trim();
const PADDING_TOP = input.get('PADDING_TOP') as number;
Expand All @@ -40,18 +43,18 @@ const FILM_FUNC: ThemeFunc = (photo: Photo, input: ThemeOptionInput, store: Stor
];

context.textAlign = 'right';
context.font = `100px digital-7`;
context.font = `100px ${FONT_FAMILY}`;
for (let i = 0; i < datas.length; i++) {
const data = datas[i];
context.fillText(data.value, canvas.width - 100, canvas.height - 100 - i * 100);
const width = context.measureText(data.value).width;
context.font = `70px digital-7`;
context.fillText(data.key, canvas.width - 100 - width - 20, canvas.height - 105 - i * 100);
context.font = `100px digital-7`;
context.font = `60px ${FONT_FAMILY}`;
context.fillText(data.key, canvas.width - 100 - width - 20, canvas.height - 110 - i * 100);
context.font = `100px ${FONT_FAMILY}`;
}
}

context.font = `70px digital-7`;
context.font = `70px ${FONT_FAMILY}`;
context.textAlign = 'left';
context.fillText(
[store.showLensModel ? store.overrideLensModel || photo.lensModel : null]
Expand All @@ -69,6 +72,18 @@ const FILM_FUNC: ThemeFunc = (photo: Photo, input: ThemeOptionInput, store: Stor
100,
canvas.height - 205
);
if (photo.takenAt) {
context.font = `50px ${FONT_FAMILY}`;
const takenAt = new Date(photo.takenAt);
context.fillText(
`${takenAt.getFullYear()}-${(takenAt.getMonth() + 1).toString().padStart(2, '0')}-${takenAt.getDate().toString().padStart(2, '0')} ${takenAt.getHours().toString().padStart(2, '0')}:${takenAt
.getMinutes()
.toString()
.padStart(2, '0')}:${takenAt.getSeconds().toString().padStart(2, '0')}`,
100,
canvas.height - 305
);
}

return canvas;
};
Expand Down
14 changes: 11 additions & 3 deletions web/src/themes/10_LIGHTROOM/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { ThemeOption, ThemeOptionInput } from '../../pages/theme/types/theme-opt
import Font from '../../fonts';

const LIGHTROOM_OPTIONS: ThemeOption[] = [
{ id: 'ARTIST', type: 'string', default: 'Your Name', description: 'your name' },
{ id: 'BACKGROUND_COLOR', type: 'color', default: '#1f1f1f', description: '#ffffff is white, #000000 is black' },
{ id: 'PADDING_TOP', type: 'number', default: 50, description: 'px' },
{ id: 'PADDING_BOTTOM', type: 'number', default: 150, description: 'px' },
Expand All @@ -20,7 +19,6 @@ const LIGHTROOM_OPTIONS: ThemeOption[] = [
];

const LIGHTROOM_FUNC: ThemeFunc = (photo: Photo, input: ThemeOptionInput, store: Store) => {
const ARTIST = (input.get('ARTIST') as string).trim();
const BACKGROUND_COLOR = (input.get('BACKGROUND_COLOR') as string).trim();
const PADDING_TOP = input.get('PADDING_TOP') as number;
const PADDING_BOTTOM = input.get('PADDING_BOTTOM') as number;
Expand Down Expand Up @@ -60,7 +58,17 @@ const LIGHTROOM_FUNC: ThemeFunc = (photo: Photo, input: ThemeOptionInput, store:
);

context.textAlign = 'right';
context.fillText(ARTIST, canvas.width - PADDING_RIGHT, canvas.height - PADDING_BOTTOM / 2);
if (photo.takenAt) {
const takenAt = new Date(photo.takenAt);
context.fillText(
`${takenAt.getFullYear()}年${(takenAt.getMonth() + 1).toString().padStart(2, '0')}月${takenAt.getDate().toString().padStart(2, '0')}日 ${takenAt.getHours().toString().padStart(2, '0')}:${takenAt
.getMinutes()
.toString()
.padStart(2, '0')}:${takenAt.getSeconds().toString().padStart(2, '0')}`,
canvas.width - PADDING_RIGHT,
canvas.height - PADDING_BOTTOM / 2
);
}

return canvas;
};
Expand Down
77 changes: 77 additions & 0 deletions web/src/themes/16_SIMPLE/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Photo from '../../core/photo';
import { Store } from '../../store';
import sandbox from '../../core/drawing/sandbox';
import { ThemeFunc } from '../../core/drawing/theme';
import { ThemeOption, ThemeOptionInput } from '../../pages/theme/types/theme-option';
import Font from '../../fonts';

const SIMPLE_OPTIONS: ThemeOption[] = [
{ id: 'LABEL', type: 'string', default: '@username', description: 'ex. @username' },
{ id: 'FONT_FAMILY', type: 'select', options: ['Barlow', ...Object.values(Font)], default: 'Barlow', description: 'ex. din-alternate-bold, digital-7, Barlow, Arial, sans-serif' },
{ id: 'PADDING_INSIDE', type: 'boolean', default: false, description: 'enable to use inside padding' },
{ id: 'PADDING_TOP', type: 'number', default: 100, description: 'px' },
{ id: 'PADDING_BOTTOM', type: 'number', default: 400, description: 'px' },
{ id: 'PADDING_LEFT', type: 'number', default: 100, description: 'px' },
{ id: 'PADDING_RIGHT', type: 'number', default: 100, description: 'px' },
];

const SIMPLE_FUNC: ThemeFunc = (photo: Photo, input: ThemeOptionInput, store: Store) => {
const LABEL = (input.get('LABEL') as string).trim();
const FONT_FAMILY = (input.get('FONT_FAMILY') as string).trim();
const PADDING_INSIDE = input.get('PADDING_INSIDE') as boolean;
const PADDING_TOP = input.get('PADDING_TOP') as number;
const PADDING_BOTTOM = input.get('PADDING_BOTTOM') as number;
const PADDING_LEFT = input.get('PADDING_LEFT') as number;
const PADDING_RIGHT = input.get('PADDING_RIGHT') as number;

const canvas = sandbox(photo, {
targetRatio: store.ratio,
notCroppedMode: store.notCroppedMode,
backgroundColor: '#ffffff',
padding: PADDING_INSIDE ? { top: 0, right: 0, bottom: 0, left: 0 } : { top: PADDING_TOP, right: PADDING_RIGHT, bottom: PADDING_BOTTOM, left: PADDING_LEFT },
});

const context = canvas.getContext('2d')!;
context.fillStyle = '#a0a0a0';
context.textAlign = 'center';
context.textBaseline = 'middle';

context.font = `300 ${40}px ${FONT_FAMILY}`;
context.fillText(LABEL, canvas.width / 2, canvas.height - 60);

context.textAlign = 'left';
context.fillStyle = '#000000';
context.font = `700 ${100}px ${FONT_FAMILY}`;
const makerWidth = context.measureText(photo.make + ' ').width;
context.font = `300 ${100}px ${FONT_FAMILY}`;
const modelWidth = context.measureText(photo.model).width;
context.font = `700 ${100}px ${FONT_FAMILY}`;
context.fillText(photo.make, canvas.width / 2 - (makerWidth + modelWidth) / 2, canvas.height - PADDING_BOTTOM / 2 - 100);
context.font = `300 ${100}px ${FONT_FAMILY}`;
context.fillText(photo.model, canvas.width / 2 - (makerWidth + modelWidth) / 2 + makerWidth, canvas.height - PADDING_BOTTOM / 2 - 100);

context.textAlign = 'center';
context.fillStyle = '#a0a0a0';

if (photo.takenAt) {
context.font = `300 ${30}px ${FONT_FAMILY}`;
const takenAt = new Date(photo.takenAt);
context.fillText(
`${takenAt.getFullYear()}年${(takenAt.getMonth() + 1).toString().padStart(2, '0')}月${takenAt.getDate().toString().padStart(2, '0')}日 ${takenAt.getHours().toString().padStart(2, '0')}:${takenAt
.getMinutes()
.toString()
.padStart(2, '0')}:${takenAt.getSeconds().toString().padStart(2, '0')}`,
canvas.width / 2,
canvas.height - PADDING_BOTTOM / 2 + 80
);
}

if (!store.disableExposureMeter) {
context.font = `300 ${50}px ${FONT_FAMILY}`;
context.fillText([`${photo.iso}`, `${photo.focalLength}`, `${photo.fNumber}`, `${photo.exposureTime}`].filter(Boolean).join(' ∙ '), canvas.width / 2, canvas.height - PADDING_BOTTOM / 2);
}

return canvas;
};

export { SIMPLE_FUNC, SIMPLE_OPTIONS };
2 changes: 2 additions & 0 deletions web/src/themes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CUSTOM_TWO_LINE_FUNC, CUSTOM_TWO_LINE_OPTIONS } from './12_CUSTOM_TWO_L
import { TIP_FUNC, TIP_OPTIONS } from './13_TIP';
import { POSTER_FUNC, POSTER_OPTIONS } from './14_POSTER';
import { CINEMASCOPE_FUNC, CINEMASCOPE_OPTIONS } from './15_CINEMASCOPE';
import { SIMPLE_FUNC, SIMPLE_OPTIONS } from './16_SIMPLE';

type AcceptInputType = string | number | boolean;

Expand Down Expand Up @@ -44,6 +45,7 @@ const useThemeStore = create<ThemeStore>((set) => ({
const themes = [
{ name: 'No frame', func: NO_FRAME_THEME_FUNC, options: NO_FRAME_OPTIONS },
{ name: 'Just frame', func: JUST_FRAME_FUNC, options: JUST_FRAME_OPTIONS },
{ name: 'Simple', func: SIMPLE_FUNC, options: SIMPLE_OPTIONS },
{ name: 'Strap', func: STRAP_FUNC, options: STRAP_OPTIONS },
{ name: 'One line', func: ONE_LINE_FUNC, options: ONE_LINE_OPTIONS },
{ name: 'Two line', func: TWO_LINE_FUNC, options: TWO_LINE_OPTIONS },
Expand Down