mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
feat(studio): decorated templates, 21 covers and 16 spreads laid on any page in the book theme
A decorated template is a whole designed page kept as data: full document elements in fractions of the page they were drawn on, with roles for the photos and words a page brings, links to the theme for its colours and faces, groups for its lockups (a title with its date line, a badge with its words, a stamp with its postmark) and an archetype with tags. Seven cover archetypes in three variants each and sixteen double spreads ship in shared/src/book/templates/data. Laying a page out on one pours its photos into the photo roles in order and its words into the title, subtitle, date, story and caption roles, takes the words it has none for from the book or the journal entry, sets the template in the book theme, scales it to the page format with its bleeds on the bleed of that page, and parks what has no room, a date line the date mark was not made of included. A text set on a colour is made to read there at the size it was fitted to, on a band of ink or paper when neither reads. A mark set as text, such as a large quotation mark, is design to every layout and never taken for the words of a page. A fitted text keeps the size it was drawn at as its ceiling, so a shorter title grows back, and every fitted text is now solved with eight percent of headroom on letter width: set in the browser, the bold condensed faces and several scripts run that much wider than the letter measure, and a fit at exactly the measure wrapped its last word out of the frame. The layout operation of edit_book_page and revise_book_design takes the new ids as a checked string and keeps the name of a locked group, list_book_layouts lists them with archetype, tags and roles, and the design prompt names them. A sibling extract script reads a page drawn in the Studio back into the format, roles, tints and groups kept.
This commit is contained in:
@@ -67,6 +67,9 @@ describe('bookCardModel', () => {
|
||||
it('FE-AI-BOOKPAGE-011: a layout is named by its template, its grid, its own name or as laid out again', () => {
|
||||
expect(layoutLabel({ kind: 'template', id: 'two-up', name: null }, t)).toBe('journey.studio.tpl.twoUp');
|
||||
expect(layoutLabel({ kind: 'template', id: 'cover-full', name: null }, t)).toBe('journey.studio.tpl.coverFull');
|
||||
expect(layoutLabel({ kind: 'template', id: 'postcard-houses', name: null }, t)).toBe(
|
||||
'studio.templates.name.postcardHouses'
|
||||
);
|
||||
expect(layoutLabel({ kind: 'grid', id: 'two-across', name: null }, t)).toBe('assistant.card.book.grid {"count":2}');
|
||||
expect(layoutLabel({ kind: 'saved', id: 'lay-1', name: 'Mine' }, t)).toBe('Mine');
|
||||
expect(layoutLabel({ kind: 'saved', id: 'lay-1', name: null }, t)).toBe('lay-1');
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type BookSpread,
|
||||
findBookGrid,
|
||||
findBookTemplate,
|
||||
findDecoratedTemplate,
|
||||
setBookCover,
|
||||
} from '@trek/shared';
|
||||
|
||||
@@ -87,7 +88,7 @@ export function layoutLabel(layout: BookCardLayout | null, t: Translate): string
|
||||
const grid = findBookGrid(layout.id);
|
||||
return grid ? t('assistant.card.book.grid', { count: grid.cells.length }) : null;
|
||||
}
|
||||
const template = findBookTemplate(layout.id);
|
||||
const template = findBookTemplate(layout.id) ?? findDecoratedTemplate(layout.id);
|
||||
return template ? t(template.labelKey) : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Turn pages drawn in the Studio into decorated templates.
|
||||
*
|
||||
* Reads the book of a journey and writes the pages named into a data file of
|
||||
* the decorated templates (shared/src/book/templates/data/), in the format
|
||||
* shared/src/book/templates/template-format.ts describes: every measure a
|
||||
* fraction of the page it was drawn on, the pictures and the words taken out,
|
||||
* the roles and the theme links kept. The reading is shared's
|
||||
* `bookTemplateFromSpread`, so what this writes is what its spec holds.
|
||||
*
|
||||
* How a drawn page says what is what:
|
||||
*
|
||||
* - Name a layer after its role in the layers panel (title, subtitle, date,
|
||||
* body, caption, photo:1, photo:2 and on, map). A text bound to the journal
|
||||
* takes the role of its source without a name, a date mark takes date, a
|
||||
* map takes map, and the photo frames left take the next photo roles in
|
||||
* paint order.
|
||||
* - Group the elements of a lockup (a title with its date line, a badge with
|
||||
* its words, a stamp with its postmark): the group comes along and is laid
|
||||
* as one group of the page.
|
||||
* - Link colours and faces to the book's theme in the inspector. With the
|
||||
* book themed, a colour the document links nowhere (a border, an outline,
|
||||
* a pattern, a sticker's slots) that is exactly one of the theme's is kept
|
||||
* as a tint, so the template follows the theme of any book it is laid in.
|
||||
*
|
||||
* Usage, from server/:
|
||||
*
|
||||
* node scripts/extract-decorated-templates.cjs <journeyId> <archetype> <id>[,<id>...] [out]
|
||||
*
|
||||
* One id per page to take, in book order (the cover first when it is
|
||||
* named), each the template's id; `-` skips a page. The tags start as the
|
||||
* archetype and the description as a line to replace: write both, and the
|
||||
* names in every locale (studio.templates.name.<id in camel case>), before
|
||||
* the file is committed. Run Prettier on it, then build shared and read the
|
||||
* templates' spec.
|
||||
*/
|
||||
const Database = require('better-sqlite3');
|
||||
const fs = require('fs');
|
||||
|
||||
const [journeyArg, archetype, idList, out] = process.argv.slice(2);
|
||||
if (!journeyArg || !archetype || !idList) {
|
||||
console.error('usage: node scripts/extract-decorated-templates.cjs <journeyId> <archetype> <id>[,<id>...] [out]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const {
|
||||
BOOK_COVER_ARCHETYPES,
|
||||
bookDecoratedTemplateSchema,
|
||||
bookTemplateFromSpread,
|
||||
normalizeBookDocument,
|
||||
} = require('@trek/shared');
|
||||
|
||||
if (!BOOK_COVER_ARCHETYPES.includes(archetype)) {
|
||||
console.error(`archetype must be one of ${BOOK_COVER_ARCHETYPES.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const db = new Database('data/travel.db', { readonly: true });
|
||||
const row = db.prepare('SELECT document FROM journey_books WHERE journey_id = ?').get(Number(journeyArg));
|
||||
db.close();
|
||||
if (!row) throw new Error(`no book on journey ${journeyArg}`);
|
||||
|
||||
// Through the contract on the way in, so every element carries every field the document has today.
|
||||
const doc = normalizeBookDocument(JSON.parse(row.document));
|
||||
const ids = idList.split(',');
|
||||
|
||||
const templates = doc.spreads.flatMap((spread, i) => {
|
||||
const id = ids[i];
|
||||
if (!id || id === '-') return [];
|
||||
const tpl = bookTemplateFromSpread(spread, doc.page, {
|
||||
id,
|
||||
archetype,
|
||||
tags: [archetype],
|
||||
descr: 'Describe in one sentence what the page shows.',
|
||||
theme: doc.theme ?? null,
|
||||
});
|
||||
const checked = bookDecoratedTemplateSchema.safeParse(tpl);
|
||||
if (!checked.success) throw new Error(`${id}: ${checked.error.message}`);
|
||||
return [tpl];
|
||||
});
|
||||
|
||||
const name = `EXTRACTED_${archetype.replace(/-/g, '_').toUpperCase()}`;
|
||||
const file = `import type { BookDecoratedTemplate } from '../template-format';
|
||||
|
||||
/**
|
||||
* Decorated templates drawn in the Studio (journey ${journeyArg}) and read out
|
||||
* by server/scripts/extract-decorated-templates.cjs: fractions of the page
|
||||
* they were drawn on, in the format of template-format.ts.
|
||||
*/
|
||||
export const ${name}: readonly BookDecoratedTemplate[] = ${JSON.stringify(templates, null, 2)};
|
||||
`;
|
||||
|
||||
const target = out || `../shared/src/book/templates/data/extracted-${archetype}.data.ts`;
|
||||
fs.writeFileSync(target, file);
|
||||
console.log(`wrote ${templates.length} templates to ${target}`);
|
||||
@@ -26,6 +26,7 @@ The energy decides the rest:
|
||||
- Loud: lively pushed further: bigger type, stronger colour, more layers, a title that fills the cover; never two heroes on one page.
|
||||
- A deliberate overlap is a design; an accidental one (two straight photos or texts of a size half over each other) is a fault.
|
||||
- Vary the beats, never three alike in a row; a quiet page for a long story, grids for bursts.
|
||||
- A decorated template (list_book_layouts, under decorated) lays a whole designed page with one layout op: the page's photos and words fill its roles in the book's theme; start a cover or a chapter opener from one, then refine it with style.
|
||||
Type scale, in points for a 210 mm page and scaled with the page width:
|
||||
- Cover title in a display face, 72 to 180 pt, 12 to 30 % of the page height; title to subtitle at least 3:1. For a big cover raise type.display (up to 6.5, about 180 pt); keep type.scale between 0.9 and 1.1 so body stays 9 to 11 pt.
|
||||
- Date lines in spaced caps, 7 to 9 pt, tracking 0.25 to 0.4; an accent word over a title in a script (caveat, pacifico, vibes, marker) or the contrasting face (playfair italic), 1.2 to 1.6 times its cap height, with contrast, an outline or a shadow.
|
||||
|
||||
@@ -121,7 +121,7 @@ export class JourneyBookMcp {
|
||||
|
||||
@Tool({
|
||||
name: 'list_book_layouts',
|
||||
description: `The layouts a page of a journey's book can take: the templates for inner double spreads and for single pages (the cover, first, last and back) with their photo and text slots, the photo grids with their cell count, and the book's own saved layouts. role narrows the answer to inner or single. ${LAYOUT_NOTES.templates} ${LAYOUT_NOTES.grids}`,
|
||||
description: `The layouts a page of a journey's book can take: the templates for inner double spreads and for single pages (the cover, first, last and back) with their photo and text slots, the decorated templates under decorated (whole designed pages, each with its archetype, tags, one line of what it looks like, its photo frames and the roles its words go into), the photo grids with their cell count, and the book's own saved layouts. role narrows the answer to inner or single. ${LAYOUT_NOTES.templates} ${LAYOUT_NOTES.decorated} ${LAYOUT_NOTES.grids}`,
|
||||
inputSchema: { journeyId: idSchema, ...listBookLayoutsArgs },
|
||||
annotations: TOOL_ANNOTATIONS_READONLY,
|
||||
when: journeyAddonOn,
|
||||
@@ -196,7 +196,7 @@ export class JourneyBookMcp {
|
||||
|
||||
@Tool({
|
||||
name: 'edit_book_page',
|
||||
description: `Change one page with 1 to ${BOOK_OP_LIMITS.opsPerCall} operations, run in order, all or nothing: layout (a template, grid, saved layout, or auto to lay the page out again from its entry), place_photo (a photo of the journey's gallery into a frame by elementId, by its slot among the page's frames, or into a new frame), photo_look (the crop as fit and focal point, mask, frame style, filter, radius), swap_photos, text (by elementId, or by role heading, body or meta, which creates the text when the page has none), caption (under a photo, bound to its caption in the journal unless words are given), frame (move, resize, turn), remove (parked with the page by default, park false deletes), background (a colour, or a paint: a colour with alpha, a linear or radial gradient, or one of twelve patterns; null is the paper), add (a new photo frame, text, shape, icon, sticker from list_book_assets, line, map, figures, country outlines, mark or list, by what it is: its look follows with style, naming the id given to it in add; a line runs through points on the spread in mm; the journey's own values such as the route, figures and countries come from the journey, never from you), style (the look of one element of its kind: opacity, colours, a shape's paint, fonts, strokes, a cut edge on a shape or photo such as a stamp's perforation, a line's points and the like, for a text also its case, vertical place, outline, hard shadow, marker band, curve on an arc or a circle, a fill of a gallery photo or hollow letters, and shrink to fit, for a photo its border, hard shadow, zoom, mirror, adjustments, duotone and cutout, and for a line its heads, dashes, a wave or an ornament; ${BOOK_THEME_LINK_HINT}), arrange (to front, forward, backward, to back), align and distribute (several elements by their edges or centres, against each other, the page or the safe area), lock, duplicate, fill (a photo to the bleed of its page or the spread, behind everything), copy_from (elements of another page), refresh_travel (travel elements to the journey's current figures), add_grid (a photo grid as empty frames in a box), group and ungroup (two or more elements picked, moved, aligned and arranged as one, such as a title lockup; align and distribute move a group as one box), name (an element or a group, as the layers panel shows it), hide (left off the page and out of print, or shown again) and add_lockup (one of the Studio's text styles: a set title, badge, postmark, stamp, quote, caption or chapter mark of a few texts and shapes, its words given by role, every text fitted to its frame, placed as one group). A locked element refuses any operation that would change it but lock, ungroup, name and hide. The road a map draws between stops is a straight or bowed line; roads are not routed. ${LAYOUT_NOTES.templates} ${LAYOUT_NOTES.grids} ${UNITS} With returnPrevious the answer also holds the page as it was, under previous_spreads, and a digest of each page as the write left it under page_digests (for restore_book_page expectedPages). ${WRITE_ANSWER}`,
|
||||
description: `Change one page with 1 to ${BOOK_OP_LIMITS.opsPerCall} operations, run in order, all or nothing: layout (a built-in or decorated template, grid, saved layout, or auto to lay the page out again from its entry), place_photo (a photo of the journey's gallery into a frame by elementId, by its slot among the page's frames, or into a new frame), photo_look (the crop as fit and focal point, mask, frame style, filter, radius), swap_photos, text (by elementId, or by role heading, body or meta, which creates the text when the page has none), caption (under a photo, bound to its caption in the journal unless words are given), frame (move, resize, turn), remove (parked with the page by default, park false deletes), background (a colour, or a paint: a colour with alpha, a linear or radial gradient, or one of twelve patterns; null is the paper), add (a new photo frame, text, shape, icon, sticker from list_book_assets, line, map, figures, country outlines, mark or list, by what it is: its look follows with style, naming the id given to it in add; a line runs through points on the spread in mm; the journey's own values such as the route, figures and countries come from the journey, never from you), style (the look of one element of its kind: opacity, colours, a shape's paint, fonts, strokes, a cut edge on a shape or photo such as a stamp's perforation, a line's points and the like, for a text also its case, vertical place, outline, hard shadow, marker band, curve on an arc or a circle, a fill of a gallery photo or hollow letters, and shrink to fit, for a photo its border, hard shadow, zoom, mirror, adjustments, duotone and cutout, and for a line its heads, dashes, a wave or an ornament; ${BOOK_THEME_LINK_HINT}), arrange (to front, forward, backward, to back), align and distribute (several elements by their edges or centres, against each other, the page or the safe area), lock, duplicate, fill (a photo to the bleed of its page or the spread, behind everything), copy_from (elements of another page), refresh_travel (travel elements to the journey's current figures), add_grid (a photo grid as empty frames in a box), group and ungroup (two or more elements picked, moved, aligned and arranged as one, such as a title lockup; align and distribute move a group as one box), name (an element or a group, as the layers panel shows it), hide (left off the page and out of print, or shown again) and add_lockup (one of the Studio's text styles: a set title, badge, postmark, stamp, quote, caption or chapter mark of a few texts and shapes, its words given by role, every text fitted to its frame, placed as one group). A locked element refuses any operation that would change it but lock, ungroup, name and hide. The road a map draws between stops is a straight or bowed line; roads are not routed. ${LAYOUT_NOTES.templates} ${LAYOUT_NOTES.grids} ${UNITS} With returnPrevious the answer also holds the page as it was, under previous_spreads, and a digest of each page as the write left it under page_digests (for restore_book_page expectedPages). ${WRITE_ANSWER}`,
|
||||
inputSchema: { journeyId: idSchema, ...editBookPageArgs },
|
||||
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
|
||||
when: journeyAddonOn,
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Decorated templates through the book tools (JBTPL-*): list_book_layouts
|
||||
* and the layout operation of edit_book_page, run through JourneyBookMcp on
|
||||
* JourneyBookModule over a snapshot test db.
|
||||
*
|
||||
* What it pins: the layouts answer lists the decorated templates of both page
|
||||
* kinds with their archetype, tags, photo frames and word roles, and the role
|
||||
* narrows it; a cover laid on a decorated template carries the template's
|
||||
* design and the book's title, and a spread template on the cover or an id the
|
||||
* catalogue does not hold is refused with nothing written.
|
||||
*/
|
||||
import type { McpContext } from '../../../src/nest-mcp';
|
||||
import { JourneyBookMcp } from '../../../src/nest/journey/journey-book.mcp';
|
||||
import { JourneyBookModule } from '../../../src/nest/journey/journey-book.module';
|
||||
import { createSnapshotTestDb } from '../../helpers/db-mock';
|
||||
import { addJourneyContributor, makeJourney } from '../../helpers/factories/journeys';
|
||||
import { makeUser } from '../../helpers/factories/users';
|
||||
import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createTestModule, type TestModule } from '../../helpers/test-module';
|
||||
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
||||
import { bookOverviewSchema, bookWriteResultSchema } from '@trek/shared';
|
||||
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const db = createSnapshotTestDb();
|
||||
let t: TestModule;
|
||||
let orm: TestOrm;
|
||||
let mcp: JourneyBookMcp;
|
||||
|
||||
beforeAll(async () => {
|
||||
t = await createTestModule({ db, imports: [JourneyBookModule] });
|
||||
mcp = t.get(JourneyBookMcp);
|
||||
orm = await createTestOrm(db);
|
||||
});
|
||||
beforeEach(() => {
|
||||
resetTestDb(db);
|
||||
orm.clear();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await orm.close();
|
||||
await t.close();
|
||||
db.close();
|
||||
});
|
||||
|
||||
const ctx = (userId: number) => ({ userId }) as McpContext;
|
||||
const json = (result: { content: Array<{ type: string; text: string }> }) => JSON.parse(result.content[0]?.text ?? '');
|
||||
|
||||
interface Row {
|
||||
id: string;
|
||||
archetype: string;
|
||||
tags: string[];
|
||||
photoSlots: number;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
/** A journey with a blank book, its title, and the ids of its cover and its one double spread. */
|
||||
async function bookFor(ownerId: number) {
|
||||
const journey = await makeJourney(orm, ownerId, { title: 'Azores' });
|
||||
await mcp.createBook({ journeyId: journey.id }, ctx(ownerId));
|
||||
const overview = bookOverviewSchema.parse(json(await mcp.getBook({ journeyId: journey.id }, ctx(ownerId))));
|
||||
const cover = overview.spreads.find((s) => s.role === 'cover')?.id ?? '';
|
||||
const inner = overview.spreads.find((s) => s.role === 'inner')?.id ?? '';
|
||||
return { journey, cover, inner, version: overview.version };
|
||||
}
|
||||
|
||||
describe('decorated templates in list_book_layouts', () => {
|
||||
it('JBTPL-001: lists the decorated covers and spreads to any member, with archetype, tags, frames and roles', async () => {
|
||||
const { user: owner } = await makeUser(orm);
|
||||
const { user: viewer } = await makeUser(orm);
|
||||
const { journey } = await bookFor(owner.id);
|
||||
await addJourneyContributor(orm, journey.id, viewer.id, 'viewer');
|
||||
|
||||
const all = json(await mcp.listLayouts({ journeyId: journey.id }, ctx(viewer.id)));
|
||||
const single = all.decorated.single as Row[];
|
||||
const inner = all.decorated.inner as Row[];
|
||||
expect(single).toHaveLength(21);
|
||||
expect(inner).toHaveLength(16);
|
||||
expect(new Set(single.map((r) => r.archetype)).size).toBe(7);
|
||||
expect(single.find((r) => r.id === 'grid-badge-circle')).toMatchObject({
|
||||
archetype: 'grid-badge',
|
||||
photoSlots: 4,
|
||||
roles: ['subtitle', 'title', 'date'],
|
||||
});
|
||||
expect(inner.find((r) => r.id === 'polaroid-stack')?.tags).toContain('polaroid');
|
||||
expect(all.notes.decorated).toMatch(/whole designed page/);
|
||||
|
||||
const narrowed = json(await mcp.listLayouts({ journeyId: journey.id, role: 'single' }, ctx(owner.id)));
|
||||
expect([narrowed.decorated.single.length, narrowed.decorated.inner]).toEqual([21, []]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('a page laid out on a decorated template', () => {
|
||||
it('JBTPL-002: edit_book_page lays the cover out on a decorated template, in the book title, with its design', async () => {
|
||||
const { user: owner } = await makeUser(orm);
|
||||
const { journey, cover } = await bookFor(owner.id);
|
||||
const written = bookWriteResultSchema.parse(
|
||||
json(
|
||||
await mcp.editBookPage(
|
||||
{ journeyId: journey.id, spreadId: cover, ops: [{ op: 'layout', template: 'minimal-serif-classic' }] },
|
||||
ctx(owner.id),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(written.spread_ids).toEqual([cover]);
|
||||
const page = json(await mcp.describeBookPage({ journeyId: journey.id, spreadId: cover }, ctx(owner.id)));
|
||||
const kinds = page.elements.map((e: { kind: string }) => e.kind);
|
||||
expect(kinds).toEqual(['photo', 'text', 'line']);
|
||||
expect(page.elements.find((e: { kind: string }) => e.kind === 'text')?.text).toBe('Azores');
|
||||
});
|
||||
|
||||
it('JBTPL-003: refuses a double spread template on the cover, and an id no template has, writing nothing', async () => {
|
||||
const { user: owner } = await makeUser(orm);
|
||||
const { journey, cover, inner, version } = await bookFor(owner.id);
|
||||
await expect(
|
||||
mcp.editBookPage(
|
||||
{ journeyId: journey.id, spreadId: cover, ops: [{ op: 'layout', template: 'polaroid-stack' }] },
|
||||
ctx(owner.id),
|
||||
),
|
||||
).rejects.toThrow(/polaroid-stack is a double-spread template/);
|
||||
await expect(
|
||||
mcp.editBookPage(
|
||||
{ journeyId: journey.id, spreadId: inner, ops: [{ op: 'layout', template: 'no-such-design' }] },
|
||||
ctx(owner.id),
|
||||
),
|
||||
).rejects.toThrow(/no such layout/);
|
||||
expect(json(await mcp.getBook({ journeyId: journey.id }, ctx(owner.id))).version).toBe(version);
|
||||
});
|
||||
|
||||
it('JBTPL-004: lays a double spread out on a decorated template, the date of its stop kept for its words', async () => {
|
||||
const { user: owner } = await makeUser(orm);
|
||||
const { journey, inner } = await bookFor(owner.id);
|
||||
const written = bookWriteResultSchema.parse(
|
||||
json(
|
||||
await mcp.editBookPage(
|
||||
{ journeyId: journey.id, spreadId: inner, ops: [{ op: 'layout', template: 'stamp-row' }] },
|
||||
ctx(owner.id),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(written.spread_ids).toEqual([inner]);
|
||||
const page = json(await mcp.describeBookPage({ journeyId: journey.id, spreadId: inner }, ctx(owner.id)));
|
||||
const frames = page.elements.filter((e: { kind: string }) => e.kind === 'photo');
|
||||
expect(frames).toHaveLength(4);
|
||||
expect(page.elements.some((e: { kind: string }) => e.kind === 'sticker')).toBe(true);
|
||||
// The blank page has no words and no entry: the title role is left off rather than set in nothing.
|
||||
expect(page.elements.some((e: { kind: string; text?: string }) => e.kind === 'text' && !e.text)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatBookCoords } from '../book-text';
|
||||
import { bookDateMarkValue, formatBookCoords } from '../book-text';
|
||||
import {
|
||||
MAX_BOOK_COUNTRIES,
|
||||
MAX_COUNTRY_NAME,
|
||||
@@ -339,11 +339,8 @@ export function placeMap(
|
||||
|
||||
/** The day of the month as a numeral with its month beneath: a mark rather than a line of information. */
|
||||
export function dateMark(newId: NewId, iso: string | null, locale: string, frame: BookFrame): BookElement | null {
|
||||
if (!iso) return null;
|
||||
const d = new Date(`${iso}T00:00:00`);
|
||||
if (Number.isNaN(d.getTime())) return null;
|
||||
const value = { text: String(d.getDate()), sub: d.toLocaleDateString(locale, { month: 'long' }).toUpperCase() };
|
||||
return badgeEl(newId, frame, 'date', value, { style: 'stacked' });
|
||||
const value = bookDateMarkValue(iso, locale);
|
||||
return value ? badgeEl(newId, frame, 'date', value, { style: 'stacked' }) : null;
|
||||
}
|
||||
|
||||
/** A full-bleed frame: out past the trim on every side the page is cut on. */
|
||||
|
||||
@@ -4,8 +4,16 @@ import {
|
||||
applyBookTemplate,
|
||||
classifyBookTexts,
|
||||
findBookTemplate,
|
||||
isBookOrnamentText,
|
||||
} from './book-layouts';
|
||||
import { bookPageSetupSchema, type BookElement, type BookPageSetup } from './book.schema';
|
||||
import { applySavedBookLayout, bookLayoutFromSpread } from './book-saved-layouts';
|
||||
import {
|
||||
bookPageSetupSchema,
|
||||
bookSpreadSchema,
|
||||
bookTextElementSchema,
|
||||
type BookElement,
|
||||
type BookPageSetup,
|
||||
} from './book.schema';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
@@ -207,4 +215,47 @@ describe('which text plays which part', () => {
|
||||
const roles = classifyBookTexts([t('a', 'Short', 9), t('b', 'Longer words', 9)]);
|
||||
expect([roles.heading, roles.body?.id, roles.meta?.id]).toEqual([null, 'b', 'a']);
|
||||
});
|
||||
|
||||
it('BOOKLAY-018: never takes a mark set as text, a large quotation mark, for the heading or the story', () => {
|
||||
expect(isBookOrnamentText(t('q', '\u201c', 140))).toBe(true);
|
||||
expect(isBookOrnamentText(t('n', 'No. 7', 9))).toBe(false);
|
||||
expect(isBookOrnamentText(t('e', ' ', 9))).toBe(false);
|
||||
const roles = classifyBookTexts([
|
||||
t('q', '\u201c', 140),
|
||||
t('h', 'Sintra', 30),
|
||||
t('d', '\u2605 \u2605 \u2605', 9),
|
||||
t('b', 'A long story', 10),
|
||||
]);
|
||||
expect([roles.heading?.id, roles.body?.id, roles.meta]).toEqual(['h', 'b', null]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('a mark set as text on a page laid out again', () => {
|
||||
const text = (id: string, words: string, size: number) =>
|
||||
bookTextElementSchema.parse({ id, kind: 'text', frame: { x: 10, y: 10, w: 40, h: 40 }, text: words, size });
|
||||
const quoted = bookSpreadSchema.parse({
|
||||
id: 'sp',
|
||||
role: 'inner',
|
||||
elements: [text('q', '\u201c', 140), text('h', 'Sintra', 30), text('b', 'Up through the fog to the palace.', 10)],
|
||||
});
|
||||
|
||||
it('BOOKLAY-019: goes with the old design on a built-in layout, neither set as words nor parked', () => {
|
||||
const hero = findBookTemplate('hero-story');
|
||||
if (!hero) throw new Error('hero-story is missing');
|
||||
const next = applyBookTemplate(quoted, hero, page);
|
||||
const words = next.elements.flatMap((e) => (e.kind === 'text' ? [e.text] : []));
|
||||
expect(words).toContain('Sintra');
|
||||
expect(words).not.toContain('\u201c');
|
||||
expect(next.parked.some((e) => e.kind === 'text' && e.text === '\u201c')).toBe(false);
|
||||
});
|
||||
|
||||
it('BOOKLAY-020: stays the design of a saved layout, and is no box for the words of the page it is laid on', () => {
|
||||
const layout = bookLayoutFromSpread(quoted, page, 'Quote');
|
||||
const other = bookSpreadSchema.parse({ id: 'sp2', role: 'inner', elements: [text('t', 'Porto', 30)] });
|
||||
const next = applySavedBookLayout(other, layout, page);
|
||||
const texts = next.elements.flatMap((e) => (e.kind === 'text' ? [{ text: e.text, size: e.size }] : []));
|
||||
expect(texts).toContainEqual({ text: '\u201c', size: 140 });
|
||||
expect(texts).toContainEqual({ text: 'Porto', size: 30 });
|
||||
expect(next.parked).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -429,6 +429,15 @@ function textEl(
|
||||
} as BookElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* A text of marks and no words, such as a large opening quotation mark: a
|
||||
* piece of the design like a shape, so a layout never takes it for the
|
||||
* page's title or story, and it goes with the old design.
|
||||
*/
|
||||
export function isBookOrnamentText(el: BookElement): boolean {
|
||||
return el.kind === 'text' && el.text.trim() !== '' && !/[\p{L}\p{N}]/u.test(el.text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Which of a spread's texts is its heading, its story and its date line.
|
||||
*
|
||||
@@ -444,11 +453,12 @@ export function classifyBookTexts(texts: readonly BookElement[]): {
|
||||
body: BookElement | null;
|
||||
meta: BookElement | null;
|
||||
} {
|
||||
const words = texts.filter((t) => !isBookOrnamentText(t));
|
||||
const heading =
|
||||
[...texts]
|
||||
[...words]
|
||||
.filter((t) => t.kind === 'text' && t.size >= 14)
|
||||
.sort((a, b) => (b.kind === 'text' ? b.size : 0) - (a.kind === 'text' ? a.size : 0))[0] ?? null;
|
||||
const rest = texts.filter((t) => t !== heading);
|
||||
const rest = words.filter((t) => t !== heading);
|
||||
const body =
|
||||
[...rest].sort((a, b) => (b.kind === 'text' ? b.text.length : 0) - (a.kind === 'text' ? a.text.length : 0))[0] ??
|
||||
null;
|
||||
@@ -471,7 +481,7 @@ export function applyBookTemplate(spread: BookSpread, tpl: BookTemplate, page: B
|
||||
// queue up behind them.
|
||||
const pool = [...spread.elements, ...(spread.parked ?? [])];
|
||||
const photos = pool.filter((e) => e.kind === 'photo');
|
||||
const texts = pool.filter((e) => e.kind === 'text');
|
||||
const texts = pool.filter((e) => e.kind === 'text' && !isBookOrnamentText(e));
|
||||
const { heading, body, meta } = classifyBookTexts(texts);
|
||||
|
||||
let pi = 0;
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
bookTextElementSchema,
|
||||
} from './book.schema';
|
||||
import { PHOTO_PICK } from './photo-pick';
|
||||
import { isBookTemplateId } from './templates/template-catalogue';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -85,10 +86,17 @@ export const bookTemplateIdSchema = z
|
||||
export const bookCoverTemplateIdSchema = z
|
||||
.enum(idsOf(BOOK_COVER_TEMPLATES))
|
||||
.describe('A built-in cover or back layout by id; list_book_layouts says what each one holds.');
|
||||
/** Either set: which one fits is the page's question, answered when the operation runs. */
|
||||
/**
|
||||
* A layout of either set, built-in or decorated: which one fits is the page's
|
||||
* question, answered when the operation runs. A string checked against the
|
||||
* catalogue rather than an enum, so the decorated set's ids stay out of the
|
||||
* tool schema; `list_book_layouts` names them.
|
||||
*/
|
||||
export const bookAnyTemplateIdSchema = z
|
||||
.enum([...idsOf(BOOK_TEMPLATES), ...idsOf(BOOK_COVER_TEMPLATES)])
|
||||
.describe('A built-in layout by id: the cover-* ones fit a single page, the rest a double page.');
|
||||
.string()
|
||||
.max(40)
|
||||
.refine(isBookTemplateId, { message: 'no such layout; list_book_layouts names them' })
|
||||
.describe('A layout id from list_book_layouts: a built-in or a decorated template, of the page kind it lists.');
|
||||
|
||||
const text = bookTextElementSchema.shape;
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ import {
|
||||
type BookSpread,
|
||||
type BookTextElement,
|
||||
} from './book.schema';
|
||||
import { findDecoratedTemplate } from './templates/template-catalogue';
|
||||
import { opDecoratedLayout } from './templates/template-op';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -182,12 +184,17 @@ function opLayout(spread: BookSpread, op: Op<'layout'>, env: Env): BookSpread {
|
||||
if (elements.length > MAX_SPREAD_ELEMENTS) {
|
||||
throw failOp('too_many_elements', `the layout and the locked elements exceed ${MAX_SPREAD_ELEMENTS}`, env.opIndex);
|
||||
}
|
||||
return { ...next, elements };
|
||||
// A locked group keeps its name, also through a layout that writes the page's groups anew.
|
||||
const have = new Set((next.groups ?? []).map((g) => g.id));
|
||||
const lost = (spread.groups ?? []).filter((g) => !have.has(g.id) && locked.some(({ el }) => el.group === g.id));
|
||||
return { ...next, elements, ...(lost.length ? { groups: [...(next.groups ?? []), ...lost] } : {}) };
|
||||
}
|
||||
|
||||
function layoutFree(spread: BookSpread, op: Op<'layout'>, env: Env): BookSpread {
|
||||
const single = spread.role !== 'inner';
|
||||
if (op.template !== undefined) {
|
||||
const decorated = findDecoratedTemplate(op.template);
|
||||
if (decorated) return opDecoratedLayout(spread, decorated, env);
|
||||
const tpl = findBookTemplate(op.template);
|
||||
if (!tpl) throw failOp('layout_not_found', `no layout ${op.template}`, env.opIndex);
|
||||
if ((tpl.role === 'single') !== single) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { bookElementId } from './book-ids';
|
||||
import { isBookOrnamentText } from './book-layouts';
|
||||
import {
|
||||
MAX_SPREAD_ELEMENTS,
|
||||
type BookElement,
|
||||
@@ -69,10 +70,14 @@ export function applySavedBookLayout(spread: BookSpread, layout: BookLayout, pag
|
||||
const sy = page.pageHeight / layout.pageHeight;
|
||||
const pool = [...spread.elements, ...(spread.parked ?? [])];
|
||||
const photos = pool.filter((e) => e.kind === 'photo' && e.photoId != null);
|
||||
const texts = pool.filter((e) => e.kind === 'text' && e.text.trim()).sort((a, b) => textSize(b) - textSize(a));
|
||||
// A mark set as text, a quotation mark, is design on either side: never the spread's words, never a box for them.
|
||||
const isWords = (e: BookElement) => e.kind === 'text' && !!e.text.trim() && !isBookOrnamentText(e);
|
||||
const texts = pool.filter(isWords).sort((a, b) => textSize(b) - textSize(a));
|
||||
|
||||
// Which of the layout's text boxes takes which of the spread's words.
|
||||
const slots = layout.elements.filter((e) => e.kind === 'text').sort((a, b) => textSize(b) - textSize(a));
|
||||
const slots = layout.elements
|
||||
.filter((e) => e.kind === 'text' && !isBookOrnamentText(e))
|
||||
.sort((a, b) => textSize(b) - textSize(a));
|
||||
const wordsFor = new Map<BookElement, BookElement>();
|
||||
slots.forEach((slot, i) => {
|
||||
const words = texts[i];
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
bookCasedText,
|
||||
bookDrawnLines,
|
||||
bookTextFits,
|
||||
bookTextRunMm,
|
||||
fitBookTexts,
|
||||
fitSpreadTexts,
|
||||
solveBookTextFit,
|
||||
@@ -87,6 +88,15 @@ describe('the fit solver', () => {
|
||||
expect(solveBookTextFit(filled)).toBeLessThan(200);
|
||||
expect(bookTextFits({ ...filled, text: ' ' })).toBe(true);
|
||||
});
|
||||
|
||||
it('BKFIT-011: leaves room for type that sets wider than the average letter of its family', () => {
|
||||
const el = text({ text: 'Lisbon', frame: { x: 0, y: 0, w: 80, h: 200 }, size: 200 });
|
||||
const size = solveBookTextFit(el);
|
||||
const measured = bookTextRunMm({ ...el, size }, 'Lisbon');
|
||||
expect(measured * 1.08).toBeLessThanOrEqual(el.frame.w + 0.01);
|
||||
expect(measured * 1.08).toBeGreaterThan(el.frame.w * 0.97);
|
||||
expect(bookTextFits({ ...el, size, frame: { ...el.frame, w: measured } })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fitting a page', () => {
|
||||
|
||||
@@ -10,8 +10,9 @@ import type { BookDocument, BookElement, BookSpread, BookTextElement } from './b
|
||||
* `size`, the largest size up to it at which the words hold the frame. The
|
||||
* renderer reads `size` like any other text, so the page stays synchronous and
|
||||
* a reader from before fitting existed draws the fitted size, which is the one
|
||||
* that was designed. The solver uses the same letter measure as the overflow
|
||||
* check, so a text it fitted is never reported as overflowing.
|
||||
* that was designed. The solver uses the overflow check's letter measure with
|
||||
* `FIT_HEADROOM` on top, so a text it fitted is never reported as overflowing,
|
||||
* and the words still hold the frame in the type the page really sets.
|
||||
*
|
||||
* Who runs it: the Studio after every change to the document, and the edit
|
||||
* engine after a page's operations, both through `fitSpreadTexts`. A size that
|
||||
@@ -23,6 +24,18 @@ import type { BookDocument, BookElement, BookSpread, BookTextElement } from './b
|
||||
/** The smallest size the solver goes to: the document's own floor. */
|
||||
const MIN_SIZE = 4;
|
||||
|
||||
/**
|
||||
* The headroom on letter width a text is fitted with. The letter measure is a
|
||||
* family's average letter, and real type runs wider in places: set in the
|
||||
* browser, the bold weights of the condensed faces and several scripts came
|
||||
* out up to 9 % wider than it, capitals of the heavy display faces about as
|
||||
* much. A fit solved at exactly the measure lands on the edge of a line
|
||||
* break, so a few percent too wide wraps the last word onto a line the frame
|
||||
* has no room for. Eight percent keeps the words inside the frame and costs a
|
||||
* fitted text well under one percent of its size on average.
|
||||
*/
|
||||
const FIT_HEADROOM = 1.08;
|
||||
|
||||
const ptToMm = (pt: number) => (pt * 25.4) / 72;
|
||||
|
||||
/** The words as they are cased on the page: what the measure counts and the SVG draws. */
|
||||
@@ -45,25 +58,22 @@ export function bookTextRunMm(el: BookTextElement, run: string): number {
|
||||
return [...run].length * bookLetterMm(el);
|
||||
}
|
||||
|
||||
/** Whether a text at its current size holds its frame, as the checks measure it. */
|
||||
/** Whether a text at its current size holds its frame, measured with the fit's headroom (`FIT_HEADROOM`). */
|
||||
export function bookTextFits(el: BookTextElement): boolean {
|
||||
if (!el.text.trim()) return true;
|
||||
const letter = bookLetterMm(el, FIT_HEADROOM);
|
||||
const run = (s: string) => [...s].length * letter;
|
||||
if (bookTextIsDrawn(el)) {
|
||||
const lines = bookDrawnLines(el);
|
||||
const widest = Math.max(0, ...lines.map((l) => bookTextRunMm(el, l)));
|
||||
const widest = Math.max(0, ...lines.map(run));
|
||||
if (el.curve) {
|
||||
const path = bookTextCurvePath(el.curve, el.frame.w, el.frame.h, ptToMm(el.size));
|
||||
return widest <= path.length * 0.98;
|
||||
}
|
||||
return widest <= el.frame.w && lines.length * ptToMm(el.size) * el.leading <= el.frame.h * 1.02;
|
||||
}
|
||||
const longest = Math.max(
|
||||
0,
|
||||
...bookCasedText(el)
|
||||
.split(/\s+/)
|
||||
.map((w) => bookTextRunMm(el, w)),
|
||||
);
|
||||
return longest <= el.frame.w && textHeightNeeded(el) <= el.frame.h * 1.02;
|
||||
const longest = Math.max(0, ...bookCasedText(el).split(/\s+/).map(run));
|
||||
return longest <= el.frame.w && textHeightNeeded(el, FIT_HEADROOM) <= el.frame.h * 1.02;
|
||||
}
|
||||
|
||||
/** The size a fitted text is set at: its ceiling when that holds, else the largest quarter point that does. */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { bookPath } from './book-path';
|
||||
import { bookCoordValue, formatBookCoords, formatBookDate } from './book-text';
|
||||
import { bookCoordValue, bookDateMarkValue, formatBookCoords, formatBookDate } from './book-text';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
@@ -30,6 +30,15 @@ describe('book text', () => {
|
||||
it('BOOKTXT-004: keeps a point as the raw value a coordinates binding compares', () => {
|
||||
expect(bookCoordValue(63.4027, -19.0448)).toBe('63.4027,-19.0448');
|
||||
});
|
||||
|
||||
it('BOOKTXT-008: sets a date mark as the day numeral and the month in capitals, and nothing for what is no date', () => {
|
||||
expect(bookDateMarkValue('2026-06-03', 'de-DE')).toEqual({ text: '3', sub: 'JUNI' });
|
||||
expect(bookDateMarkValue('2026-06-03T10:00:00Z', 'en')).toEqual({ text: '3', sub: 'JUNE' });
|
||||
// A language tag the platform cannot read spells the month in English rather than failing the page.
|
||||
expect(bookDateMarkValue('2026-06-03', 'en_US')).toEqual({ text: '3', sub: 'JUNE' });
|
||||
expect(bookDateMarkValue('June', 'en')).toBeNull();
|
||||
expect(bookDateMarkValue(null, 'en')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('book path', () => {
|
||||
|
||||
@@ -59,6 +59,28 @@ export function formatBookDate(iso: string | null, locale: string): string {
|
||||
return d.toLocaleDateString(locale, { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
}
|
||||
|
||||
/**
|
||||
* The entry's day as a date mark shows it: the day of the month as a numeral
|
||||
* and the month spelled out in capitals beneath it; null for anything that is
|
||||
* not a date. The auto layout, the travel marks and the decorated templates
|
||||
* all set a mark with this.
|
||||
*/
|
||||
export function bookDateMarkValue(
|
||||
iso: string | null | undefined,
|
||||
locale: string,
|
||||
): { text: string; sub: string } | null {
|
||||
if (!iso || !/^\d{4}-\d{2}-\d{2}/.test(iso)) return null;
|
||||
const d = new Date(`${iso.slice(0, 10)}T00:00:00`);
|
||||
if (Number.isNaN(d.getTime())) return null;
|
||||
let month: string;
|
||||
try {
|
||||
month = d.toLocaleDateString(locale, { month: 'long' });
|
||||
} catch {
|
||||
month = d.toLocaleDateString('en', { month: 'long' });
|
||||
}
|
||||
return { text: String(d.getDate()), sub: month.toUpperCase() };
|
||||
}
|
||||
|
||||
/**
|
||||
* What a point is, as one string: the raw value a coordinates binding keeps,
|
||||
* so re-reading it compares the fact behind the words and not the words.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { statsValues } from './auto/auto-elements';
|
||||
import type { AutoEntry } from './auto/auto-types';
|
||||
import { BOOK_MAP_SOURCES } from './book-map-sources';
|
||||
import { formatBookCoords } from './book-text';
|
||||
import { bookDateMarkValue, formatBookCoords } from './book-text';
|
||||
import {
|
||||
MAX_BOOK_COUNTRIES,
|
||||
MAX_COUNTRY_NAME,
|
||||
@@ -330,10 +330,8 @@ export function bookBadgeValue(
|
||||
return day ? { text: bookDayText(ctx.labels.day, day), sub: '', code: null } : null;
|
||||
}
|
||||
case 'date': {
|
||||
const d = date ? new Date(`${date}T00:00:00`) : null;
|
||||
if (!d || Number.isNaN(d.getTime())) return null;
|
||||
const month = d.toLocaleDateString(ctx.locale, { month: 'long' }).toUpperCase();
|
||||
return { text: String(d.getDate()), sub: month, code: null };
|
||||
const mark = bookDateMarkValue(date, ctx.locale);
|
||||
return mark ? { ...mark, code: null } : null;
|
||||
}
|
||||
case 'coords': {
|
||||
if (lat == null || lng == null) return null;
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type BookSpread,
|
||||
} from './book.schema';
|
||||
import { bookEdgeWords, bookLineWords } from './describe-lines';
|
||||
import { decoratedTemplateRow, decoratedTemplatesFor } from './templates/template-catalogue';
|
||||
import { bookThemeLine, themeBackgroundWords, themeLinkWords } from './theme/book-theme-describe';
|
||||
|
||||
/**
|
||||
@@ -335,6 +336,8 @@ const BOOK_LAYOUT_NOTES = {
|
||||
"A template or saved layout re-pours the photos and texts of the page into its slots and parks what it has no room for; the page's own shapes, maps, badges, stats, lists, icons and stickers are dropped (a saved layout brings its own), and the write answer names them in dropped.",
|
||||
grids:
|
||||
'A grid re-pours every photo of the page, placed or parked, into its cells and parks the rest; the old frames go, empty ones too. Texts, shapes and travel elements stay where they are.',
|
||||
decorated:
|
||||
"A decorated template is a whole designed page: the page's photos fill its frames in order, its words go into the roles the row names (title, subtitle, date, body, caption) and a map into its map, its colours and faces follow the book's theme, and it brings its own shapes, stickers and lines.",
|
||||
} as const;
|
||||
|
||||
/** `list_book_layouts`: what a page can be laid out on, for a double spread, a single page, or both. */
|
||||
@@ -344,6 +347,10 @@ export function listBookLayouts(doc: BookDocument | null, page: BookPageSetup, r
|
||||
notes: BOOK_LAYOUT_NOTES,
|
||||
inner: want('inner') ? BOOK_TEMPLATES.map((t) => templateRow(t, page)) : [],
|
||||
single: want('single') ? BOOK_COVER_TEMPLATES.map((t) => templateRow(t, page)) : [],
|
||||
decorated: {
|
||||
inner: want('inner') ? decoratedTemplatesFor('inner').map(decoratedTemplateRow) : [],
|
||||
single: want('single') ? decoratedTemplatesFor('single').map(decoratedTemplateRow) : [],
|
||||
},
|
||||
grids: BOOK_GRIDS.map((g) => ({ id: g.id, cells: g.cells.length })),
|
||||
saved: (doc?.layouts ?? [])
|
||||
.filter((l) => want(l.role))
|
||||
|
||||
@@ -0,0 +1,500 @@
|
||||
import type { BookDecoratedTemplate } from '../template-format';
|
||||
|
||||
/**
|
||||
* The cut-out covers: a saturated page, paper cut-outs in two tones behind
|
||||
* and in front of a portrait photo in a thick white border, a condensed
|
||||
* title and a spaced date line. Drawn in cutout.
|
||||
*
|
||||
* Drawn on a 210 mm page and kept as fractions of it (template-format.ts),
|
||||
* in the colours and faces of the theme named above; their links let any
|
||||
* other theme recolour them.
|
||||
*/
|
||||
export const COVERS_CUTOUT: readonly BookDecoratedTemplate[] = [
|
||||
{
|
||||
id: 'cutout-matisse',
|
||||
labelKey: 'studio.templates.name.cutoutMatisse',
|
||||
kind: 'single',
|
||||
archetype: 'cutout',
|
||||
tags: ['cut-out', 'matisse', 'shapes', 'portrait', 'colour', 'lively'],
|
||||
descr:
|
||||
'A saturated page with paper cut-outs, a coral leaf and a sand blob behind and a frond and a star in front of a portrait photo in a thick white border; the date spaced at the head, a condensed title across the foot.',
|
||||
background: '#2445b8',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 's1',
|
||||
frame: { x: -0.1238, y: 0.3429, w: 0.5333, h: 0.481 },
|
||||
rotation: -16,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'leaf-monstera',
|
||||
fill: '#ff7a59',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
frame: { x: 0.581, y: 0.0762, w: 0.4095, h: 0.4762 },
|
||||
rotation: 8,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'organic-2',
|
||||
fill: '#f1d9b5',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent2',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.2952, y: 0.1524, w: 0.4095, h: 0.5429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0262, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 's4',
|
||||
frame: { x: 0.6095, y: 0.5143, w: 0.3048, h: 0.2857 },
|
||||
rotation: 22,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'leaf-palm',
|
||||
fill: '#f1d9b5',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent2',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's5',
|
||||
frame: { x: 0.219, y: 0.5905, w: 0.1333, h: 0.1286 },
|
||||
rotation: -10,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'star-organic',
|
||||
fill: '#ff7a59',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'date',
|
||||
frame: { x: 0.0762, y: 0.0667, w: 0.8476, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'oswald',
|
||||
size: 0.0429,
|
||||
weight: 500,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.5,
|
||||
color: '#fdf8ef',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0429,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'caption',
|
||||
frame: { x: 0.0762, y: 0.1714, w: 0.1905, h: 0.0857 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'inter',
|
||||
size: 0.0333,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.35,
|
||||
tracking: 0.04,
|
||||
color: '#fdf8ef',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0333,
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'title',
|
||||
frame: { x: 0.0476, y: 0.7143, w: 0.9048, h: 0.2286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'shoulders',
|
||||
size: 0.6667,
|
||||
weight: 800,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 0.85,
|
||||
tracking: 0,
|
||||
color: '#fdf8ef',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.6667,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'cutout-arch',
|
||||
labelKey: 'studio.templates.name.cutoutArch',
|
||||
kind: 'single',
|
||||
archetype: 'cutout',
|
||||
tags: ['cut-out', 'arch', 'sun', 'shapes', 'colour', 'lively'],
|
||||
descr:
|
||||
'A condensed title at the head over a spaced date, a photo cut into an arch with a thick white border, a paper sun behind it, weed and a long leaf cut out beside it.',
|
||||
background: '#2445b8',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 's1',
|
||||
keep: true,
|
||||
frame: { x: 0.5143, y: 0.3143, w: 0.5333, h: 0.5238 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'sun-cutout',
|
||||
fill: '#f1d9b5',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent2',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
frame: { x: 0.0381, y: 0.3333, w: 0.1905, h: 0.5 },
|
||||
rotation: -6,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'seaweed',
|
||||
fill: '#ff7a59',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.2857, y: 0.3048, w: 0.4286, h: 0.6286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: 'arch',
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0238, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 's4',
|
||||
frame: { x: 0.7048, y: 0.6476, w: 0.1143, h: 0.5143 },
|
||||
rotation: 18,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'leaf-long',
|
||||
fill: '#9fd3ff',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent3',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'title',
|
||||
frame: { x: 0.0476, y: 0.0476, w: 0.9048, h: 0.2 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'shoulders',
|
||||
size: 0.5714,
|
||||
weight: 800,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 0.85,
|
||||
tracking: 0,
|
||||
color: '#fdf8ef',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.5714,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'date',
|
||||
frame: { x: 0.0762, y: 0.2476, w: 0.8476, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'oswald',
|
||||
size: 0.0429,
|
||||
weight: 500,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.5,
|
||||
color: '#fdf8ef',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0429,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'caption',
|
||||
frame: { x: 0.0762, y: 0.8762, w: 0.1905, h: 0.0762 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'inter',
|
||||
size: 0.0333,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.35,
|
||||
tracking: 0.04,
|
||||
color: '#fdf8ef',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'bottom',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0333,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'cutout-collage',
|
||||
labelKey: 'studio.templates.name.cutoutCollage',
|
||||
kind: 'single',
|
||||
archetype: 'cutout',
|
||||
tags: ['cut-out', 'collage', 'shapes', 'portrait', 'colour', 'lively'],
|
||||
descr:
|
||||
'A tilted portrait photo in a thick white border on a sky-coloured blob, an oak leaf and a coral cut-out over its edges, the date at the head and a condensed title at the foot on the left.',
|
||||
background: '#2445b8',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 's1',
|
||||
frame: { x: 0.4571, y: 0.0476, w: 0.6667, h: 0.5857 },
|
||||
rotation: -6,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'organic-1',
|
||||
fill: '#9fd3ff',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent3',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.4762, y: 0.1429, w: 0.4286, h: 0.5619 },
|
||||
rotation: 3,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0262, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
frame: { x: 0.3524, y: 0.3429, w: 0.1619, h: 0.3524 },
|
||||
rotation: -20,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'leaf-oak',
|
||||
fill: '#f1d9b5',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent2',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's4',
|
||||
frame: { x: 0.7905, y: 0.5905, w: 0.3048, h: 0.2095 },
|
||||
rotation: -12,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'coral-2',
|
||||
fill: '#ff7a59',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'date',
|
||||
frame: { x: 0.0762, y: 0.0857, w: 0.3429, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'oswald',
|
||||
size: 0.0429,
|
||||
weight: 500,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.5,
|
||||
color: '#fdf8ef',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0429,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'title',
|
||||
frame: { x: 0.0571, y: 0.7143, w: 0.5905, h: 0.2286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'shoulders',
|
||||
size: 0.4571,
|
||||
weight: 800,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 0.85,
|
||||
tracking: 0,
|
||||
color: '#fdf8ef',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'bottom',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.4571,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'caption',
|
||||
frame: { x: 0.6667, y: 0.8476, w: 0.2667, h: 0.0857 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'inter',
|
||||
size: 0.0333,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'right',
|
||||
leading: 1.35,
|
||||
tracking: 0.04,
|
||||
color: '#fdf8ef',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'bottom',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0333,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,625 @@
|
||||
import type { BookDecoratedTemplate } from '../template-format';
|
||||
|
||||
/**
|
||||
* The grid and badge covers: four photos two by two with a cross of paper
|
||||
* between them, and a round badge at the centre with the title and the
|
||||
* year. Drawn in grid-badge.
|
||||
*
|
||||
* Drawn on a 210 mm page and kept as fractions of it (template-format.ts),
|
||||
* in the colours and faces of the theme named above; their links let any
|
||||
* other theme recolour them.
|
||||
*/
|
||||
export const COVERS_GRID_BADGE: readonly BookDecoratedTemplate[] = [
|
||||
{
|
||||
id: 'grid-badge-circle',
|
||||
labelKey: 'studio.templates.name.gridBadgeCircle',
|
||||
kind: 'single',
|
||||
archetype: 'grid-badge',
|
||||
tags: ['grid', 'four-photos', 'badge', 'circle', 'clean', 'graphic'],
|
||||
descr:
|
||||
'Four bleed photos two by two with a 5 mm white cross between them, a white circle at the centre with a hairline ring, a small spaced line, the title in heavy capitals and the year over a red rule.',
|
||||
background: null,
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: -0.0143, y: -0.0143, w: 0.5024, h: 0.5024 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
role: 'photo:2',
|
||||
frame: { x: 0.5119, y: -0.0143, w: 0.5024, h: 0.5024 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
role: 'photo:3',
|
||||
frame: { x: -0.0143, y: 0.5119, w: 0.5024, h: 0.5024 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:4',
|
||||
frame: { x: 0.5119, y: 0.5119, w: 0.5024, h: 0.5024 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 's5',
|
||||
keep: true,
|
||||
frame: { x: 0.2905, y: 0.2905, w: 0.419, h: 0.419 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'shape',
|
||||
shape: 'ellipse',
|
||||
fill: '#ffffff',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'paper',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's6',
|
||||
keep: true,
|
||||
frame: { x: 0.3095, y: 0.3095, w: 0.381, h: 0.381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'shape',
|
||||
shape: 'ellipse',
|
||||
fill: null,
|
||||
gradient: 'none',
|
||||
stroke: '#141414',
|
||||
strokeToken: 'ink',
|
||||
strokeWidth: 0.0019,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'subtitle',
|
||||
keep: true,
|
||||
on: 'paper',
|
||||
frame: { x: 0.3619, y: 0.3762, w: 0.2762, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.031,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.32,
|
||||
color: '#5b5b5b',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.031,
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'title',
|
||||
keep: true,
|
||||
on: 'paper',
|
||||
frame: { x: 0.3429, y: 0.419, w: 0.3143, h: 0.1286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.1429,
|
||||
weight: 800,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 0.95,
|
||||
tracking: 0,
|
||||
color: '#141414',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1429,
|
||||
},
|
||||
{
|
||||
id: 's9',
|
||||
keep: true,
|
||||
frame: { x: 0.4619, y: 0.5571, w: 0.0762, h: 0.0038 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#e2412f',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't10',
|
||||
role: 'date',
|
||||
keep: true,
|
||||
on: 'paper',
|
||||
frame: { x: 0.381, y: 0.5714, w: 0.2381, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0405,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.4,
|
||||
color: '#141414',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0405,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'grid-badge-stamp',
|
||||
labelKey: 'studio.templates.name.gridBadgeStamp',
|
||||
kind: 'single',
|
||||
archetype: 'grid-badge',
|
||||
tags: ['grid', 'four-photos', 'badge', 'scalloped', 'curved-text', 'graphic'],
|
||||
descr:
|
||||
'Four photos two by two inside a white margin with a 4 mm gutter, a black scalloped badge at the centre with a red ring, the subtitle running round it, the title and the year in white.',
|
||||
background: null,
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.0571, y: 0.0571, w: 0.4333, h: 0.4333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
role: 'photo:2',
|
||||
frame: { x: 0.5095, y: 0.0571, w: 0.4333, h: 0.4333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
role: 'photo:3',
|
||||
frame: { x: 0.0571, y: 0.5095, w: 0.4333, h: 0.4333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:4',
|
||||
frame: { x: 0.5095, y: 0.5095, w: 0.4333, h: 0.4333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 's5',
|
||||
keep: true,
|
||||
frame: { x: 0.2952, y: 0.2952, w: 0.4095, h: 0.4095 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'shape',
|
||||
shape: 'scallop-circle',
|
||||
fill: '#141414',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'ink',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's6',
|
||||
keep: true,
|
||||
frame: { x: 0.3238, y: 0.3238, w: 0.3524, h: 0.3524 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'shape',
|
||||
shape: 'ellipse',
|
||||
fill: null,
|
||||
gradient: 'none',
|
||||
stroke: '#e2412f',
|
||||
strokeToken: 'accent',
|
||||
strokeWidth: 0.0029,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'subtitle',
|
||||
keep: true,
|
||||
on: 'ink',
|
||||
frame: { x: 0.3381, y: 0.3381, w: 0.3238, h: 0.3238 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.031,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.3,
|
||||
color: '#ffffff',
|
||||
token: 'paper',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
curve: { type: 'circle', bend: 40, start: 0 },
|
||||
fit: 'shrink',
|
||||
fitMax: 0.031,
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'title',
|
||||
keep: true,
|
||||
on: 'ink',
|
||||
frame: { x: 0.3762, y: 0.4381, w: 0.2476, h: 0.0952 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.1143,
|
||||
weight: 800,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 0.95,
|
||||
tracking: 0,
|
||||
color: '#ffffff',
|
||||
token: 'paper',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1143,
|
||||
},
|
||||
{
|
||||
id: 't9',
|
||||
role: 'date',
|
||||
keep: true,
|
||||
on: 'ink',
|
||||
frame: { x: 0.4048, y: 0.5429, w: 0.1905, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0357,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.4,
|
||||
color: '#ffffff',
|
||||
token: 'paper',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0357,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'grid-badge-seal',
|
||||
labelKey: 'studio.templates.name.gridBadgeSeal',
|
||||
kind: 'single',
|
||||
archetype: 'grid-badge',
|
||||
tags: ['grid', 'four-photos', 'badge', 'seal', 'red', 'graphic', 'lively'],
|
||||
descr:
|
||||
'Four bleed photos two by two on a red ground that shows as a 6 mm cross between them, a white seal at the centre with a script line, the title in heavy capitals and the year.',
|
||||
background: '#e2412f',
|
||||
backgroundToken: 'accent',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: -0.0143, y: -0.0143, w: 0.5, h: 0.5 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
role: 'photo:2',
|
||||
frame: { x: 0.5143, y: -0.0143, w: 0.5, h: 0.5 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
role: 'photo:3',
|
||||
frame: { x: -0.0143, y: 0.5143, w: 0.5, h: 0.5 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:4',
|
||||
frame: { x: 0.5143, y: 0.5143, w: 0.5, h: 0.5 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 's5',
|
||||
keep: true,
|
||||
frame: { x: 0.2762, y: 0.2762, w: 0.4476, h: 0.4476 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'shape',
|
||||
shape: 'seal',
|
||||
fill: '#ffffff',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'paper',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'subtitle',
|
||||
keep: true,
|
||||
on: 'paper',
|
||||
frame: { x: 0.3429, y: 0.3429, w: 0.3143, h: 0.1048 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'allura',
|
||||
size: 0.1238,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#e2412f',
|
||||
token: 'accent',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'bottom',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1238,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'title',
|
||||
keep: true,
|
||||
on: 'paper',
|
||||
frame: { x: 0.3429, y: 0.4571, w: 0.3143, h: 0.1143 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.1333,
|
||||
weight: 800,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 0.95,
|
||||
tracking: 0,
|
||||
color: '#141414',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1333,
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'date',
|
||||
keep: true,
|
||||
on: 'paper',
|
||||
frame: { x: 0.3905, y: 0.581, w: 0.219, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'badge',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.4,
|
||||
color: '#141414',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,418 @@
|
||||
import type { BookDecoratedTemplate } from '../template-format';
|
||||
|
||||
/**
|
||||
* The minimal serif covers: a white page, a large photo with a hairline, a
|
||||
* very large thin serif title, the subtitle line, a sun between two
|
||||
* hairlines and the year. Drawn in minimal-serif.
|
||||
*
|
||||
* Drawn on a 210 mm page and kept as fractions of it (template-format.ts),
|
||||
* in the colours and faces of the theme named above; their links let any
|
||||
* other theme recolour them.
|
||||
*/
|
||||
export const COVERS_MINIMAL_SERIF: readonly BookDecoratedTemplate[] = [
|
||||
{
|
||||
id: 'minimal-serif-classic',
|
||||
labelKey: 'studio.templates.name.minimalSerifClassic',
|
||||
kind: 'single',
|
||||
archetype: 'minimal-serif',
|
||||
tags: ['minimal', 'serif', 'white', 'hairline', 'calm', 'elegant'],
|
||||
descr:
|
||||
'A white page, a wide photo with a hairline border, a very large thin serif title under it, the subtitle in spaced capitals, a sun between two hairlines and the year.',
|
||||
background: null,
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
tint: { border: 'ink' },
|
||||
frame: { x: 0.1143, y: 0.1048, w: 0.7714, h: 0.4952 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0014, color: '#222222' },
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
role: 'title',
|
||||
frame: { x: 0.0762, y: 0.6286, w: 0.8476, h: 0.1524 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'cormorant',
|
||||
size: 0.3619,
|
||||
weight: 300,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 0.95,
|
||||
tracking: 0,
|
||||
color: '#222222',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.3619,
|
||||
},
|
||||
{
|
||||
id: 't3',
|
||||
role: 'subtitle',
|
||||
frame: { x: 0.1429, y: 0.7905, w: 0.7143, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0405,
|
||||
weight: 300,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1.4,
|
||||
tracking: 0.32,
|
||||
color: '#646464',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0405,
|
||||
},
|
||||
{
|
||||
id: 'l4',
|
||||
tint: { stroke: 'accent' },
|
||||
frame: { x: 0.3429, y: 0.8429, w: 0.3143, h: 0.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'line',
|
||||
points: [
|
||||
{ x: 0, y: 0.5 },
|
||||
{ x: 1, y: 0.5 },
|
||||
],
|
||||
curve: 'straight',
|
||||
stroke: '#857d72',
|
||||
strokeWidth: 0.0012,
|
||||
dash: 'solid',
|
||||
squiggle: 'none',
|
||||
startCap: 'none',
|
||||
endCap: 'none',
|
||||
wobble: 0,
|
||||
ornament: 'sun',
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'date',
|
||||
frame: { x: 0.3333, y: 0.8905, w: 0.3333, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 300,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1.4,
|
||||
tracking: 0.4,
|
||||
color: '#222222',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'minimal-serif-portrait',
|
||||
labelKey: 'studio.templates.name.minimalSerifPortrait',
|
||||
kind: 'single',
|
||||
archetype: 'minimal-serif',
|
||||
tags: ['minimal', 'serif', 'white', 'portrait', 'hairline', 'calm', 'elegant'],
|
||||
descr:
|
||||
'A tall photo on the right with a hairline border; on the left a very large thin serif title over several lines, the subtitle in spaced capitals, a sun rule and the year.',
|
||||
background: null,
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
tint: { border: 'ink' },
|
||||
frame: { x: 0.4762, y: 0.1143, w: 0.4095, h: 0.7714 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0014, color: '#222222' },
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
role: 'title',
|
||||
frame: { x: 0.1048, y: 0.1143, w: 0.3333, h: 0.5524 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'cormorant',
|
||||
size: 0.2667,
|
||||
weight: 300,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 0.92,
|
||||
tracking: 0,
|
||||
color: '#222222',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'bottom',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.2667,
|
||||
},
|
||||
{
|
||||
id: 't3',
|
||||
role: 'subtitle',
|
||||
frame: { x: 0.1143, y: 0.6952, w: 0.3143, h: 0.0762 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 300,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.4,
|
||||
tracking: 0.32,
|
||||
color: '#646464',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'top',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 'l4',
|
||||
tint: { stroke: 'accent' },
|
||||
frame: { x: 0.1143, y: 0.8, w: 0.2476, h: 0.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'line',
|
||||
points: [
|
||||
{ x: 0, y: 0.5 },
|
||||
{ x: 1, y: 0.5 },
|
||||
],
|
||||
curve: 'straight',
|
||||
stroke: '#857d72',
|
||||
strokeWidth: 0.0012,
|
||||
dash: 'solid',
|
||||
squiggle: 'none',
|
||||
startCap: 'none',
|
||||
endCap: 'none',
|
||||
wobble: 0,
|
||||
ornament: 'sun',
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'date',
|
||||
frame: { x: 0.1143, y: 0.8524, w: 0.2857, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 300,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.4,
|
||||
tracking: 0.4,
|
||||
color: '#222222',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'minimal-serif-window',
|
||||
labelKey: 'studio.templates.name.minimalSerifWindow',
|
||||
kind: 'single',
|
||||
archetype: 'minimal-serif',
|
||||
tags: ['minimal', 'serif', 'white', 'square-photo', 'hairline', 'calm', 'elegant'],
|
||||
descr:
|
||||
'A sun rule at the head, a square photo with a hairline border in the middle of a white page, a very large thin serif title under it, the subtitle and the year in spaced capitals.',
|
||||
background: null,
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'l1',
|
||||
tint: { stroke: 'accent' },
|
||||
frame: { x: 0.381, y: 0.0667, w: 0.2381, h: 0.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'line',
|
||||
points: [
|
||||
{ x: 0, y: 0.5 },
|
||||
{ x: 1, y: 0.5 },
|
||||
],
|
||||
curve: 'straight',
|
||||
stroke: '#857d72',
|
||||
strokeWidth: 0.0012,
|
||||
dash: 'solid',
|
||||
squiggle: 'none',
|
||||
startCap: 'none',
|
||||
endCap: 'none',
|
||||
wobble: 0,
|
||||
ornament: 'sun',
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
role: 'photo:1',
|
||||
tint: { border: 'ink' },
|
||||
keep: true,
|
||||
frame: { x: 0.2714, y: 0.1238, w: 0.4571, h: 0.4571 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0014, color: '#222222' },
|
||||
},
|
||||
{
|
||||
id: 't3',
|
||||
role: 'title',
|
||||
frame: { x: 0.0667, y: 0.619, w: 0.8667, h: 0.181 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'cormorant',
|
||||
size: 0.4,
|
||||
weight: 300,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 0.95,
|
||||
tracking: 0,
|
||||
color: '#222222',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.4,
|
||||
},
|
||||
{
|
||||
id: 't4',
|
||||
role: 'subtitle',
|
||||
frame: { x: 0.1429, y: 0.819, w: 0.7143, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0405,
|
||||
weight: 300,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1.4,
|
||||
tracking: 0.32,
|
||||
color: '#646464',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0405,
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'date',
|
||||
frame: { x: 0.3333, y: 0.8762, w: 0.3333, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 300,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1.4,
|
||||
tracking: 0.4,
|
||||
color: '#222222',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,436 @@
|
||||
import type { BookDecoratedTemplate } from '../template-format';
|
||||
|
||||
/**
|
||||
* The illustrated postcard covers: a pastel page, a drawing at the centre,
|
||||
* a large display serif title, a script line and the year. Drawn in
|
||||
* postcard.
|
||||
*
|
||||
* Drawn on a 210 mm page and kept as fractions of it (template-format.ts),
|
||||
* in the colours and faces of the theme named above; their links let any
|
||||
* other theme recolour them.
|
||||
*/
|
||||
export const COVERS_POSTCARD: readonly BookDecoratedTemplate[] = [
|
||||
{
|
||||
id: 'postcard-houses',
|
||||
labelKey: 'studio.templates.name.postcardHouses',
|
||||
kind: 'single',
|
||||
archetype: 'postcard',
|
||||
tags: ['postcard', 'illustrated', 'town', 'houses', 'stamp', 'pastel', 'calm'],
|
||||
descr:
|
||||
'A pastel postcard with a ruled border: the title in a display serif and a script line at the head, a row of town houses drawn at the centre, the year under them, a photo as a perforated stamp with a postmark.',
|
||||
background: '#e3f1e8',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 's1',
|
||||
frame: { x: 0.0476, y: 0.0476, w: 0.9048, h: 0.9048 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: null,
|
||||
gradient: 'none',
|
||||
stroke: '#1f2d4d',
|
||||
strokeToken: 'ink',
|
||||
strokeWidth: 0.0021,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0.0071,
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
frame: { x: 0.0595, y: 0.0595, w: 0.881, h: 0.881 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: null,
|
||||
gradient: 'none',
|
||||
stroke: '#1f2d4d',
|
||||
strokeToken: 'ink',
|
||||
strokeWidth: 0.001,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0.0048,
|
||||
},
|
||||
{
|
||||
id: 't3',
|
||||
role: 'title',
|
||||
frame: { x: 0.0952, y: 0.1143, w: 0.8095, h: 0.1429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dmserif',
|
||||
size: 0.2857,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.2857,
|
||||
},
|
||||
{
|
||||
id: 't4',
|
||||
role: 'subtitle',
|
||||
frame: { x: 0.1143, y: 0.2571, w: 0.7714, h: 0.0857 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dancing',
|
||||
size: 0.1238,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1.1,
|
||||
tracking: 0,
|
||||
color: '#c8453d',
|
||||
token: 'accent',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1238,
|
||||
},
|
||||
{
|
||||
id: 's5',
|
||||
keep: true,
|
||||
tint: { colors: ['accent3', 'accent', 'ink', 'accent2'] },
|
||||
frame: { x: 0.2, y: 0.3619, w: 0.6, h: 0.481 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'rowHouses',
|
||||
colors: ['#f3d58a', '#c8453d', '#1f2d4d', '#ec8f6a'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'date',
|
||||
frame: { x: 0.2619, y: 0.8667, w: 0.4762, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.45,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 'p7',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.7524, y: 0.7048, w: 0.1429, h: 0.181 },
|
||||
rotation: 5,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'stamp',
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0086, color: '#ffffff' },
|
||||
edge: 'perforated',
|
||||
edgeSize: 0.0062,
|
||||
},
|
||||
{
|
||||
id: 's8',
|
||||
tint: { colors: ['ink'] },
|
||||
frame: { x: 0.6381, y: 0.7333, w: 0.181, h: 0.1048 },
|
||||
rotation: -8,
|
||||
opacity: 0.85,
|
||||
locked: false,
|
||||
group: 'stamp',
|
||||
kind: 'sticker',
|
||||
stickerId: 'postmark',
|
||||
colors: ['#1f2d4d'],
|
||||
flipX: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'postcard-coast',
|
||||
labelKey: 'studio.templates.name.postcardCoast',
|
||||
kind: 'single',
|
||||
archetype: 'postcard',
|
||||
tags: ['postcard', 'illustrated', 'sea', 'sailing', 'no-photo', 'pastel', 'calm'],
|
||||
descr:
|
||||
'An illustration without a photo: a sailing boat under the sun at the centre of a pastel page, a wave rule under it, a script line, the title in a display serif and the year.',
|
||||
background: '#e3f1e8',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 's1',
|
||||
tint: { colors: ['accent3', 'accent2'] },
|
||||
frame: { x: 0.5905, y: 0.1048, w: 0.2286, h: 0.2286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'sun',
|
||||
colors: ['#f3d58a', '#ec8f6a'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
keep: true,
|
||||
tint: { colors: ['accent', null, 'ink', null] },
|
||||
frame: { x: 0.2476, y: 0.1333, w: 0.4762, h: 0.5286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'sailboat',
|
||||
colors: ['#c8453d', '#fbf8f0', '#1f2d4d', '#4ea8de'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
tint: { colors: ['ink'] },
|
||||
frame: { x: 0.2952, y: 0.6571, w: 0.4095, h: 0.0571 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'sticker',
|
||||
stickerId: 'waveRule',
|
||||
colors: ['#1f2d4d'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 't4',
|
||||
role: 'subtitle',
|
||||
frame: { x: 0.1143, y: 0.7095, w: 0.7714, h: 0.0714 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dancing',
|
||||
size: 0.1238,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1.1,
|
||||
tracking: 0,
|
||||
color: '#c8453d',
|
||||
token: 'accent',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1238,
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'title',
|
||||
frame: { x: 0.0762, y: 0.781, w: 0.8476, h: 0.1381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dmserif',
|
||||
size: 0.2857,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.2857,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'date',
|
||||
frame: { x: 0.2619, y: 0.9286, w: 0.4762, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.45,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'postcard-peaks',
|
||||
labelKey: 'studio.templates.name.postcardPeaks',
|
||||
kind: 'single',
|
||||
archetype: 'postcard',
|
||||
tags: ['postcard', 'illustrated', 'mountains', 'round-photo', 'pastel', 'calm'],
|
||||
descr:
|
||||
'The title in a display serif and a script line at the head, a round photo with a white border at the centre, a drawn mountain range in front of its foot, the year under it.',
|
||||
background: '#e3f1e8',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 't1',
|
||||
role: 'title',
|
||||
frame: { x: 0.0857, y: 0.0762, w: 0.8286, h: 0.1429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dmserif',
|
||||
size: 0.2667,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.2667,
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
role: 'subtitle',
|
||||
frame: { x: 0.1143, y: 0.219, w: 0.7714, h: 0.0762 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dancing',
|
||||
size: 0.1238,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1.1,
|
||||
tracking: 0,
|
||||
color: '#c8453d',
|
||||
token: 'accent',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1238,
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
role: 'photo:1',
|
||||
keep: true,
|
||||
frame: { x: 0.2381, y: 0.3143, w: 0.5238, h: 0.5238 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: 'ellipse',
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0167, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 's4',
|
||||
keep: true,
|
||||
tint: { colors: ['accent2', 'ink', null, 'accent3'] },
|
||||
frame: { x: 0.2476, y: 0.6095, w: 0.5048, h: 0.3 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'mountainRange',
|
||||
colors: ['#ec8f6a', '#1f2d4d', '#f7f9fb', '#f3d58a'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'date',
|
||||
frame: { x: 0.2619, y: 0.9286, w: 0.4762, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.45,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,570 @@
|
||||
import type { BookDecoratedTemplate } from '../template-format';
|
||||
|
||||
/**
|
||||
* The poster pop covers: two blocks of colour, a heavy two-line title, a
|
||||
* tilted inset photo in a white border, a burst of rays and tropical
|
||||
* stickers. Drawn in poster-pop.
|
||||
*
|
||||
* Drawn on a 210 mm page and kept as fractions of it (template-format.ts),
|
||||
* in the colours and faces of the theme named above; their links let any
|
||||
* other theme recolour them.
|
||||
*/
|
||||
export const COVERS_POSTER_POP: readonly BookDecoratedTemplate[] = [
|
||||
{
|
||||
id: 'poster-pop-sunburst',
|
||||
labelKey: 'studio.templates.name.posterPopSunburst',
|
||||
kind: 'single',
|
||||
archetype: 'poster-pop',
|
||||
tags: ['poster', 'bold', 'two-tone', 'sunburst', 'tropical', 'stickers', 'loud'],
|
||||
descr:
|
||||
'Sunflower over coral: a burst of rays behind a tilted photo in a white border, tropical leaves and a hibiscus, a heavy two-line title with a hard shadow on the coral block.',
|
||||
background: '#ffbf3c',
|
||||
backgroundToken: 'accent2',
|
||||
elements: [
|
||||
{
|
||||
id: 's1',
|
||||
keep: true,
|
||||
frame: { x: 0.2952, y: -0.1333, w: 0.8476, h: 0.8476 },
|
||||
rotation: 0,
|
||||
opacity: 0.5,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'sunburst-24',
|
||||
fill: '#fbf1de',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'paper',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
frame: { x: -0.0143, y: 0.581, w: 1.0286, h: 0.4333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#ef4e45',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
tint: { colors: ['accent3', 'ink'] },
|
||||
frame: { x: -0.0762, y: 0.2762, w: 0.2952, h: 0.2952 },
|
||||
rotation: -24,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'monsteraLeaf',
|
||||
colors: ['#13807b', '#1d1a2f'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:1',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.4857, y: 0.1048, w: 0.3905, h: 0.4762 },
|
||||
rotation: 5,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0152, color: '#ffffff' },
|
||||
shadow: { dx: 0.0076, dy: 0.0076, color: '#1d1a2f', alpha: 0.25 },
|
||||
},
|
||||
{
|
||||
id: 's5',
|
||||
tint: { colors: ['accent', 'accent2', 'accent3', 'ink'] },
|
||||
frame: { x: 0.4095, y: 0.4381, w: 0.1619, h: 0.1619 },
|
||||
rotation: 12,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'hibiscus',
|
||||
colors: ['#ef4e45', '#ffbf3c', '#13807b', '#1d1a2f'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 's6',
|
||||
tint: { colors: ['accent3', 'ink'] },
|
||||
frame: { x: 0.8095, y: -0.0476, w: 0.2286, h: 0.2286 },
|
||||
rotation: 36,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'palmLeaf',
|
||||
colors: ['#13807b', '#1d1a2f'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'date',
|
||||
tint: { highlight: 'ink' },
|
||||
frame: { x: 0.0857, y: 0.0762, w: 0.3429, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'head',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0429,
|
||||
weight: 700,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.38,
|
||||
color: '#fbf1de',
|
||||
token: 'paper',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
highlight: { color: '#1d1a2f', alpha: 1, padX: 0.0114, padY: 0.0052, radius: 0 },
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0429,
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'subtitle',
|
||||
on: 'accent2',
|
||||
frame: { x: 0.0762, y: 0.1333, w: 0.381, h: 0.1429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'head',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'anton',
|
||||
size: 0.0857,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.05,
|
||||
tracking: 0,
|
||||
color: '#1d1a2f',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'top',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0857,
|
||||
},
|
||||
{
|
||||
id: 't9',
|
||||
role: 'title',
|
||||
on: 'accent',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.0667, y: 0.6095, w: 0.8667, h: 0.3333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'archivo',
|
||||
size: 0.3905,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 0.86,
|
||||
tracking: 0,
|
||||
color: '#fbf1de',
|
||||
token: 'paper',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
shadow: { dx: 0.0067, dy: 0.0067, color: '#1d1a2f', alpha: 1 },
|
||||
fit: 'shrink',
|
||||
fitMax: 0.3905,
|
||||
},
|
||||
{
|
||||
id: 's10',
|
||||
tint: { colors: ['paper'] },
|
||||
frame: { x: 0.8476, y: 0.4857, w: 0.0952, h: 0.0952 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'sparkles',
|
||||
colors: ['#fbf1de'],
|
||||
flipX: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'poster-pop-split',
|
||||
labelKey: 'studio.templates.name.posterPopSplit',
|
||||
kind: 'single',
|
||||
archetype: 'poster-pop',
|
||||
tags: ['poster', 'bold', 'two-tone', 'split', 'tropical', 'stickers', 'loud'],
|
||||
descr:
|
||||
'Teal and coral halves side by side, a wide tilted photo in a white border across both, a sun, a palm leaf, a hibiscus and a toucan round it, a heavy two-line title across the lower third with a hard shadow and the date on an ink band.',
|
||||
background: '#fbf1de',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 's1',
|
||||
frame: { x: -0.0143, y: -0.0143, w: 0.5143, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#13807b',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent3',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
frame: { x: 0.5, y: -0.0143, w: 0.5143, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#ef4e45',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
tint: { colors: ['accent2', 'paper'] },
|
||||
frame: { x: 0.0476, y: 0.0286, w: 0.181, h: 0.181 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'sun',
|
||||
colors: ['#ffbf3c', '#fbf1de'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 's4',
|
||||
tint: { colors: ['accent3', 'ink'] },
|
||||
frame: { x: 0.7905, y: -0.0571, w: 0.2381, h: 0.2381 },
|
||||
rotation: 34,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'palmLeaf',
|
||||
colors: ['#13807b', '#1d1a2f'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
role: 'photo:1',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.1905, y: 0.1048, w: 0.619, h: 0.4 },
|
||||
rotation: -4,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0152, color: '#ffffff' },
|
||||
shadow: { dx: 0.0076, dy: 0.0076, color: '#1d1a2f', alpha: 0.25 },
|
||||
},
|
||||
{
|
||||
id: 's6',
|
||||
tint: { colors: ['accent', 'accent2', 'accent3', 'ink'] },
|
||||
frame: { x: 0.0952, y: 0.419, w: 0.1429, h: 0.1429 },
|
||||
rotation: -14,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'hibiscus',
|
||||
colors: ['#ef4e45', '#ffbf3c', '#13807b', '#1d1a2f'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 's7',
|
||||
tint: { colors: ['ink', 'accent2', 'paper', 'accent3'] },
|
||||
frame: { x: 0.7333, y: 0.3714, w: 0.1905, h: 0.1619 },
|
||||
rotation: -6,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'toucan',
|
||||
colors: ['#1d1a2f', '#ffbf3c', '#fbf1de', '#13807b'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'title',
|
||||
on: 'accent',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.0667, y: 0.5619, w: 0.8667, h: 0.3333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'archivo',
|
||||
size: 0.381,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 0.86,
|
||||
tracking: 0,
|
||||
color: '#fbf1de',
|
||||
token: 'paper',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
shadow: { dx: 0.0067, dy: 0.0067, color: '#1d1a2f', alpha: 1 },
|
||||
fit: 'shrink',
|
||||
fitMax: 0.381,
|
||||
},
|
||||
{
|
||||
id: 't9',
|
||||
role: 'date',
|
||||
tint: { highlight: 'ink' },
|
||||
frame: { x: 0.2619, y: 0.9095, w: 0.4762, h: 0.0429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0405,
|
||||
weight: 700,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.38,
|
||||
color: '#fbf1de',
|
||||
token: 'paper',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
highlight: { color: '#1d1a2f', alpha: 1, padX: 0.0114, padY: 0.0052, radius: 0 },
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0405,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'poster-pop-stack',
|
||||
labelKey: 'studio.templates.name.posterPopStack',
|
||||
kind: 'single',
|
||||
archetype: 'poster-pop',
|
||||
tags: ['poster', 'bold', 'two-tone', 'stacked-title', 'tropical', 'stickers', 'loud'],
|
||||
descr:
|
||||
'A coral top with the title stacked in two heavy lines, a sunflower foot with a burst of rays behind a tilted photo, flowers at its corners, the subtitle and date in ink beside it.',
|
||||
background: '#ef4e45',
|
||||
backgroundToken: 'accent',
|
||||
elements: [
|
||||
{
|
||||
id: 's1',
|
||||
frame: { x: -0.0143, y: 0.5048, w: 1.0286, h: 0.5095 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#ffbf3c',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent2',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
keep: true,
|
||||
frame: { x: 0.3524, y: 0.4, w: 0.7143, h: 0.7143 },
|
||||
rotation: 0,
|
||||
opacity: 0.45,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'sunburst-12',
|
||||
fill: '#fbf1de',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'paper',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't3',
|
||||
role: 'title',
|
||||
on: 'accent',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.0667, y: 0.0667, w: 0.8667, h: 0.4 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'archivo',
|
||||
size: 0.4,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 0.86,
|
||||
tracking: 0,
|
||||
color: '#fbf1de',
|
||||
token: 'paper',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'bottom',
|
||||
shadow: { dx: 0.0067, dy: 0.0067, color: '#1d1a2f', alpha: 1 },
|
||||
fit: 'shrink',
|
||||
fitMax: 0.4,
|
||||
},
|
||||
{
|
||||
id: 's4',
|
||||
tint: { colors: ['accent3', 'ink'] },
|
||||
frame: { x: 0.8095, y: 0.4667, w: 0.2381, h: 0.2381 },
|
||||
rotation: 30,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'monsteraLeaf',
|
||||
colors: ['#13807b', '#1d1a2f'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
role: 'photo:1',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.4571, y: 0.5429, w: 0.4286, h: 0.3429 },
|
||||
rotation: 4,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0152, color: '#ffffff' },
|
||||
shadow: { dx: 0.0076, dy: 0.0076, color: '#1d1a2f', alpha: 0.25 },
|
||||
},
|
||||
{
|
||||
id: 's6',
|
||||
tint: { colors: ['accent', 'accent2', 'accent3', 'ink'] },
|
||||
frame: { x: 0.4, y: 0.4857, w: 0.1429, h: 0.1429 },
|
||||
rotation: -10,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'hibiscus',
|
||||
colors: ['#ef4e45', '#ffbf3c', '#13807b', '#1d1a2f'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 's7',
|
||||
tint: { colors: ['paper', 'accent2', 'accent'] },
|
||||
frame: { x: 0.819, y: 0.819, w: 0.1238, h: 0.1238 },
|
||||
rotation: 18,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'frangipani',
|
||||
colors: ['#fbf1de', '#ffbf3c', '#ef4e45'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'subtitle',
|
||||
on: 'accent2',
|
||||
frame: { x: 0.0762, y: 0.5714, w: 0.3429, h: 0.219 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'side',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'anton',
|
||||
size: 0.0952,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.05,
|
||||
tracking: 0,
|
||||
color: '#1d1a2f',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'top',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0952,
|
||||
},
|
||||
{
|
||||
id: 't9',
|
||||
role: 'date',
|
||||
tint: { highlight: 'ink' },
|
||||
frame: { x: 0.0857, y: 0.8762, w: 0.3333, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'side',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0405,
|
||||
weight: 700,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.38,
|
||||
color: '#fbf1de',
|
||||
token: 'paper',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
highlight: { color: '#1d1a2f', alpha: 1, padX: 0.0114, padY: 0.0052, radius: 0 },
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0405,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,529 @@
|
||||
import type { BookDecoratedTemplate } from '../template-format';
|
||||
|
||||
/**
|
||||
* The spine band covers: a bleed photo beside a band of book cloth on the
|
||||
* binding side, the title on the band. Drawn in fabric-classic.
|
||||
*
|
||||
* Drawn on a 210 mm page and kept as fractions of it (template-format.ts),
|
||||
* in the colours and faces of the theme named above; their links let any
|
||||
* other theme recolour them.
|
||||
*/
|
||||
export const COVERS_SPINE_BAND: readonly BookDecoratedTemplate[] = [
|
||||
{
|
||||
id: 'spine-band-stitched',
|
||||
labelKey: 'studio.templates.name.spineBandStitched',
|
||||
kind: 'single',
|
||||
archetype: 'spine-band',
|
||||
tags: ['cloth', 'band', 'spine', 'photo', 'calm', 'classic'],
|
||||
descr:
|
||||
'A bleed photo beside a cloth band at the spine with a stitched seam; the title runs up the band in spaced capitals, the year sits on a small label at its foot, a script line over the photo.',
|
||||
background: '#efe8db',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.1619, y: -0.0143, w: 0.8524, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
frame: { x: -0.0143, y: -0.0143, w: 0.1762, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#5f7d99',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 'l3',
|
||||
tint: { stroke: 'paper' },
|
||||
frame: { x: 0.1376, y: 0.0286, w: 0.0057, h: 0.9429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'line',
|
||||
points: [
|
||||
{ x: 0.5, y: 0 },
|
||||
{ x: 0.5, y: 1 },
|
||||
],
|
||||
curve: 'straight',
|
||||
stroke: '#efe8db',
|
||||
strokeWidth: 0.0021,
|
||||
dash: 'dashed',
|
||||
squiggle: 'none',
|
||||
startCap: 'none',
|
||||
endCap: 'none',
|
||||
wobble: 0,
|
||||
ornament: 'none',
|
||||
},
|
||||
{
|
||||
id: 't4',
|
||||
role: 'title',
|
||||
on: 'accent',
|
||||
frame: { x: -0.2762, y: 0.4333, w: 0.7143, h: 0.0952 },
|
||||
rotation: -90,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.1238,
|
||||
weight: 500,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1.1,
|
||||
tracking: 0.42,
|
||||
color: '#efe8db',
|
||||
token: 'paper',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1238,
|
||||
},
|
||||
{
|
||||
id: 's5',
|
||||
frame: { x: 0.031, y: 0.8667, w: 0.1, h: 0.0571 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'label',
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#efe8db',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'paper',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0.0038,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'date',
|
||||
frame: { x: 0.031, y: 0.8667, w: 0.1, h: 0.0571 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'label',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0357,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.25,
|
||||
color: '#2e2a26',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0357,
|
||||
},
|
||||
{
|
||||
id: 's7',
|
||||
frame: { x: 0.1619, y: 0.6, w: 0.8524, h: 0.4143 },
|
||||
rotation: 0,
|
||||
opacity: 0.62,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#2e2a26',
|
||||
gradient: 'down',
|
||||
stroke: null,
|
||||
token: 'ink',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'subtitle',
|
||||
frame: { x: 0.2381, y: 0.7619, w: 0.6952, h: 0.1619 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'sacramento',
|
||||
size: 0.1905,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'right',
|
||||
leading: 1.05,
|
||||
tracking: 0,
|
||||
color: '#efe8db',
|
||||
token: 'paper',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'bottom',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1905,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'spine-band-gingham',
|
||||
labelKey: 'studio.templates.name.spineBandGingham',
|
||||
kind: 'single',
|
||||
archetype: 'spine-band',
|
||||
tags: ['cloth', 'gingham', 'band', 'label', 'photo', 'classic'],
|
||||
descr:
|
||||
'A bleed photo beside a gingham cloth band; a cream label pasted across band and photo holds the title in a serif, a script line under it and the year in spaced capitals.',
|
||||
background: '#efe8db',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.181, y: -0.0143, w: 0.8333, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
tint: { fg: 'accent2', bg: 'paper' },
|
||||
frame: { x: -0.0143, y: -0.0143, w: 0.1952, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#b0603c',
|
||||
gradient: 'none',
|
||||
paint: { type: 'pattern', pattern: 'gingham', fg: '#b0603c', bg: '#efe8db', scale: 0.6, angle: 0 },
|
||||
stroke: null,
|
||||
token: 'accent2',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
frame: { x: 0.1019, y: 0.5495, w: 0.5333, h: 0.3048 },
|
||||
rotation: 0,
|
||||
opacity: 0.18,
|
||||
locked: false,
|
||||
group: 'label',
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#2e2a26',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'ink',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0.0048,
|
||||
},
|
||||
{
|
||||
id: 's4',
|
||||
frame: { x: 0.0952, y: 0.5429, w: 0.5333, h: 0.3048 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'label',
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#efe8db',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'paper',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0.0048,
|
||||
},
|
||||
{
|
||||
id: 's5',
|
||||
frame: { x: 0.1095, y: 0.5571, w: 0.5048, h: 0.2762 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'label',
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: null,
|
||||
gradient: 'none',
|
||||
stroke: '#b0603c',
|
||||
strokeToken: 'accent2',
|
||||
strokeWidth: 0.0014,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0.0029,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'title',
|
||||
frame: { x: 0.1333, y: 0.5762, w: 0.4571, h: 0.1048 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'label',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'cormorant',
|
||||
size: 0.1619,
|
||||
weight: 500,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#2e2a26',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1619,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'subtitle',
|
||||
on: 'paper',
|
||||
frame: { x: 0.1333, y: 0.681, w: 0.4571, h: 0.0762 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'label',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'sacramento',
|
||||
size: 0.1238,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#b0603c',
|
||||
token: 'accent2',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1238,
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'date',
|
||||
frame: { x: 0.1333, y: 0.7714, w: 0.4571, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'label',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0357,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.4,
|
||||
color: '#68635c',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0357,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'spine-band-script',
|
||||
labelKey: 'studio.templates.name.spineBandScript',
|
||||
kind: 'single',
|
||||
archetype: 'spine-band',
|
||||
tags: ['band', 'spine', 'script', 'photo', 'dark', 'lively'],
|
||||
descr:
|
||||
'A bleed photo beside a deep ink band with a terracotta trim; a large script title starts on the band and runs over the photo, the date in spaced capitals under it, the subtitle up the band.',
|
||||
background: '#efe8db',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.1714, y: -0.0143, w: 0.8429, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
frame: { x: -0.0143, y: -0.0143, w: 0.1857, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#2e2a26',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'ink',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
frame: { x: 0.1548, y: -0.0143, w: 0.0167, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#b0603c',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent2',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's4',
|
||||
frame: { x: 0.1714, y: 0.4381, w: 0.8429, h: 0.5762 },
|
||||
rotation: 0,
|
||||
opacity: 0.66,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#2e2a26',
|
||||
gradient: 'down',
|
||||
stroke: null,
|
||||
token: 'ink',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'subtitle',
|
||||
frame: { x: -0.2976, y: 0.419, w: 0.7619, h: 0.0571 },
|
||||
rotation: -90,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0429,
|
||||
weight: 500,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.38,
|
||||
color: '#efe8db',
|
||||
token: 'paper',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0429,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'title',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.0952, y: 0.5619, w: 0.8476, h: 0.2381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'sacramento',
|
||||
size: 0.4571,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#efe8db',
|
||||
token: 'paper',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'bottom',
|
||||
shadow: { dx: 0.0029, dy: 0.0029, color: '#2e2a26', alpha: 0.35 },
|
||||
fit: 'shrink',
|
||||
fitMax: 0.4571,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'date',
|
||||
frame: { x: 0.2, y: 0.819, w: 0.7143, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0405,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.45,
|
||||
color: '#efe8db',
|
||||
token: 'paper',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0405,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,640 @@
|
||||
import type { BookDecoratedTemplate } from '../template-format';
|
||||
|
||||
/**
|
||||
* The stamp collage covers: a checker band, photos cut into arches and
|
||||
* perforated stamps, a round postmark with its words on the ring. Drawn
|
||||
* in india-stamp.
|
||||
*
|
||||
* Drawn on a 210 mm page and kept as fractions of it (template-format.ts),
|
||||
* in the colours and faces of the theme named above; their links let any
|
||||
* other theme recolour them.
|
||||
*/
|
||||
export const COVERS_STAMP_COLLAGE: readonly BookDecoratedTemplate[] = [
|
||||
{
|
||||
id: 'stamp-collage-checker',
|
||||
labelKey: 'studio.templates.name.stampCollageChecker',
|
||||
kind: 'single',
|
||||
archetype: 'stamp-collage',
|
||||
tags: ['stamps', 'collage', 'checker', 'arches', 'postmark', 'three-photos', 'lively'],
|
||||
descr:
|
||||
'A magenta checker band across the head, three photos below it cut into a tall arch, a zigzag-edged print and an arch, each in a white border, the title in a fat display serif and a round postmark with its words on the ring.',
|
||||
background: '#fbf2e2',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 's1',
|
||||
tint: { fg: 'accent', bg: 'paper' },
|
||||
frame: { x: -0.0143, y: -0.0143, w: 1.0286, h: 0.4 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#bd2275',
|
||||
gradient: 'none',
|
||||
paint: { type: 'pattern', pattern: 'checker', fg: '#bd2275', bg: '#fbf2e2', scale: 1.1, angle: 0 },
|
||||
stroke: null,
|
||||
token: 'accent',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.0857, y: 0.1714, w: 0.2667, h: 0.5333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: 'arch-tall',
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0143, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
role: 'photo:2',
|
||||
frame: { x: 0.381, y: 0.2476, w: 0.2857, h: 0.3619 },
|
||||
rotation: -4,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0119, color: '#ffffff' },
|
||||
edge: 'zigzag',
|
||||
edgeSize: 0.0076,
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:3',
|
||||
frame: { x: 0.6952, y: 0.1905, w: 0.2286, h: 0.4095 },
|
||||
rotation: 4,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: 'arch',
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0119, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'title',
|
||||
frame: { x: 0.0762, y: 0.7238, w: 0.5143, h: 0.181 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'abril',
|
||||
size: 0.2667,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#2a1b2e',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'bottom',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.2667,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'caption',
|
||||
frame: { x: 0.0857, y: 0.9143, w: 0.4762, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.42,
|
||||
color: '#695c64',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 'l7',
|
||||
tint: { stroke: 'ink' },
|
||||
frame: { x: 0.5281, y: 0.7697, w: 0.2081, h: 0.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'postmark',
|
||||
kind: 'line',
|
||||
points: [
|
||||
{ x: 0, y: 0.5 },
|
||||
{ x: 1, y: 0.5 },
|
||||
],
|
||||
curve: 'straight',
|
||||
stroke: '#2a1b2e',
|
||||
strokeWidth: 0.0019,
|
||||
dash: 'solid',
|
||||
squiggle: 'wave',
|
||||
startCap: 'none',
|
||||
endCap: 'none',
|
||||
wobble: 0,
|
||||
ornament: 'none',
|
||||
},
|
||||
{
|
||||
id: 'l8',
|
||||
tint: { stroke: 'ink' },
|
||||
frame: { x: 0.5281, y: 0.8091, w: 0.2081, h: 0.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'postmark',
|
||||
kind: 'line',
|
||||
points: [
|
||||
{ x: 0, y: 0.5 },
|
||||
{ x: 1, y: 0.5 },
|
||||
],
|
||||
curve: 'straight',
|
||||
stroke: '#2a1b2e',
|
||||
strokeWidth: 0.0019,
|
||||
dash: 'solid',
|
||||
squiggle: 'wave',
|
||||
startCap: 'none',
|
||||
endCap: 'none',
|
||||
wobble: 0,
|
||||
ornament: 'none',
|
||||
},
|
||||
{
|
||||
id: 's9',
|
||||
keep: true,
|
||||
frame: { x: 0.7143, y: 0.6952, w: 0.219, h: 0.219 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'postmark',
|
||||
kind: 'shape',
|
||||
shape: 'ellipse',
|
||||
fill: null,
|
||||
gradient: 'none',
|
||||
stroke: '#2a1b2e',
|
||||
strokeToken: 'ink',
|
||||
strokeWidth: 0.0029,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's10',
|
||||
keep: true,
|
||||
frame: { x: 0.7515, y: 0.7325, w: 0.1446, h: 0.1446 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'postmark',
|
||||
kind: 'shape',
|
||||
shape: 'ellipse',
|
||||
fill: null,
|
||||
gradient: 'none',
|
||||
stroke: '#2a1b2e',
|
||||
strokeToken: 'ink',
|
||||
strokeWidth: 0.0014,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't11',
|
||||
role: 'subtitle',
|
||||
keep: true,
|
||||
frame: { x: 0.7214, y: 0.7024, w: 0.2048, h: 0.2048 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'postmark',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.031,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.28,
|
||||
color: '#2a1b2e',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
curve: { type: 'circle', bend: 40, start: 0 },
|
||||
fit: 'shrink',
|
||||
fitMax: 0.031,
|
||||
},
|
||||
{
|
||||
id: 't12',
|
||||
role: 'date',
|
||||
keep: true,
|
||||
frame: { x: 0.7625, y: 0.7785, w: 0.1227, h: 0.0526 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'postmark',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'abril',
|
||||
size: 0.0714,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#2a1b2e',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0714,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'stamp-collage-row',
|
||||
labelKey: 'studio.templates.name.stampCollageRow',
|
||||
kind: 'single',
|
||||
archetype: 'stamp-collage',
|
||||
tags: ['stamps', 'collage', 'checker', 'perforated', 'postmark', 'three-photos', 'lively'],
|
||||
descr:
|
||||
'The title in a fat display serif and the date at the head, three photos as perforated stamps in a loose row, a teal checker band under them and a postmark over the last; the subtitle on an ink band at the foot.',
|
||||
background: '#fbf2e2',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 't1',
|
||||
role: 'title',
|
||||
frame: { x: 0.0762, y: 0.0762, w: 0.8476, h: 0.1905 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'abril',
|
||||
size: 0.2857,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#2a1b2e',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.2857,
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
role: 'date',
|
||||
frame: { x: 0.1905, y: 0.2762, w: 0.619, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.42,
|
||||
color: '#695c64',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
tint: { fg: 'accent3', bg: 'paper' },
|
||||
frame: { x: -0.0143, y: 0.6667, w: 1.0286, h: 0.3476 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#12807f',
|
||||
gradient: 'none',
|
||||
paint: { type: 'pattern', pattern: 'checker', fg: '#12807f', bg: '#fbf2e2', scale: 1.1, angle: 0 },
|
||||
stroke: null,
|
||||
token: 'accent3',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:1',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.0667, y: 0.381, w: 0.2762, h: 0.3524 },
|
||||
rotation: -5,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0143, color: '#ffffff' },
|
||||
shadow: { dx: 0.0057, dy: 0.0057, color: '#2a1b2e', alpha: 0.18 },
|
||||
edge: 'perforated',
|
||||
edgeSize: 0.0067,
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
role: 'photo:2',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.3619, y: 0.3524, w: 0.2762, h: 0.3524 },
|
||||
rotation: 2,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0143, color: '#ffffff' },
|
||||
shadow: { dx: 0.0057, dy: 0.0057, color: '#2a1b2e', alpha: 0.18 },
|
||||
edge: 'perforated',
|
||||
edgeSize: 0.0067,
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
role: 'photo:3',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.6571, y: 0.3905, w: 0.2762, h: 0.3524 },
|
||||
rotation: 6,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'stamp',
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0143, color: '#ffffff' },
|
||||
shadow: { dx: 0.0057, dy: 0.0057, color: '#2a1b2e', alpha: 0.18 },
|
||||
edge: 'perforated',
|
||||
edgeSize: 0.0067,
|
||||
},
|
||||
{
|
||||
id: 's7',
|
||||
tint: { colors: ['ink'] },
|
||||
frame: { x: 0.7143, y: 0.6571, w: 0.219, h: 0.1286 },
|
||||
rotation: -10,
|
||||
opacity: 0.9,
|
||||
locked: false,
|
||||
group: 'stamp',
|
||||
kind: 'sticker',
|
||||
stickerId: 'postmark',
|
||||
colors: ['#2a1b2e'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'subtitle',
|
||||
tint: { highlight: 'ink' },
|
||||
frame: { x: 0.1905, y: 0.8571, w: 0.619, h: 0.0476 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0405,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.42,
|
||||
color: '#fbf2e2',
|
||||
token: 'paper',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
highlight: { color: '#2a1b2e', alpha: 1, padX: 0.0119, padY: 0.0048, radius: 0 },
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0405,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'stamp-collage-arches',
|
||||
labelKey: 'studio.templates.name.stampCollageArches',
|
||||
kind: 'single',
|
||||
archetype: 'stamp-collage',
|
||||
tags: ['stamps', 'collage', 'checker', 'arches', 'passport-stamp', 'two-photos', 'lively'],
|
||||
descr:
|
||||
'The title in a fat display serif and a script line at the head, two photos cut into tall arches with white borders standing on a saffron checker band, a passport stamp over the corner and the date at the foot.',
|
||||
background: '#fbf2e2',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 't1',
|
||||
role: 'title',
|
||||
frame: { x: 0.0762, y: 0.0667, w: 0.8476, h: 0.1714 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'abril',
|
||||
size: 0.2952,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#2a1b2e',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.2952,
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
role: 'subtitle',
|
||||
frame: { x: 0.1429, y: 0.2381, w: 0.7143, h: 0.0762 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'parisienne',
|
||||
size: 0.1238,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#bd2275',
|
||||
token: 'accent',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1238,
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
tint: { fg: 'accent2', bg: 'paper' },
|
||||
frame: { x: -0.0143, y: 0.5333, w: 1.0286, h: 0.1905 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#e9801a',
|
||||
gradient: 'none',
|
||||
paint: { type: 'pattern', pattern: 'checker', fg: '#e9801a', bg: '#fbf2e2', scale: 0.9, angle: 0 },
|
||||
stroke: null,
|
||||
token: 'accent2',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.1619, y: 0.3429, w: 0.2952, h: 0.5619 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: 'arch-tall',
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0143, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
role: 'photo:2',
|
||||
frame: { x: 0.5429, y: 0.3429, w: 0.2952, h: 0.5619 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: 'arch-tall',
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0143, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 's6',
|
||||
tint: { colors: ['accent'] },
|
||||
frame: { x: 0.7048, y: 0.7524, w: 0.219, h: 0.1524 },
|
||||
rotation: -12,
|
||||
opacity: 0.9,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'passportStamp',
|
||||
colors: ['#bd2275'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'date',
|
||||
frame: { x: 0.2619, y: 0.919, w: 0.4762, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'center',
|
||||
leading: 1,
|
||||
tracking: 0.42,
|
||||
color: '#2a1b2e',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,502 @@
|
||||
import type { BookDecoratedTemplate } from '../template-format';
|
||||
|
||||
/**
|
||||
* Double spreads that open a part of the journey: a panorama, a block of
|
||||
* colour, a day. Each is drawn in the theme of its archetype.
|
||||
*
|
||||
* Drawn on a 210 mm page and kept as fractions of it (template-format.ts),
|
||||
* in the colours and faces of the theme named above; their links let any
|
||||
* other theme recolour them.
|
||||
*/
|
||||
export const INNER_CHAPTERS: readonly BookDecoratedTemplate[] = [
|
||||
{
|
||||
id: 'bleed-band-caption',
|
||||
labelKey: 'studio.templates.name.bleedBandCaption',
|
||||
kind: 'inner',
|
||||
archetype: 'spine-band',
|
||||
tags: ['full-bleed', 'panorama', 'band', 'caption', 'one-photo', 'calm'],
|
||||
descr:
|
||||
'One photo across the whole spread to the bleed, a band of paper across the foot of the right page with a terracotta edge, the title in a serif, the date and the caption on it.',
|
||||
background: '#efe8db',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: -0.0143, y: -0.0143, w: 2.0286, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.7,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
frame: { x: 1.0762, y: 0.6952, w: 0.9381, h: 0.219 },
|
||||
rotation: 0,
|
||||
opacity: 0.95,
|
||||
locked: false,
|
||||
group: 'band',
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#efe8db',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'paper',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
frame: { x: 1.0762, y: 0.6952, w: 0.0119, h: 0.219 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'band',
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#b0603c',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent2',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't4',
|
||||
role: 'title',
|
||||
frame: { x: 1.1429, y: 0.7238, w: 0.781, h: 0.0952 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'band',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'cormorant',
|
||||
size: 0.1524,
|
||||
weight: 500,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#2e2a26',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1524,
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'date',
|
||||
frame: { x: 1.1429, y: 0.8333, w: 0.2952, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'band',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.4,
|
||||
color: '#68635c',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'caption',
|
||||
frame: { x: 1.4476, y: 0.8333, w: 0.4762, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'band',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'garamond',
|
||||
size: 0.0381,
|
||||
weight: 400,
|
||||
italic: true,
|
||||
align: 'right',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#2e2a26',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'two-tone-split',
|
||||
labelKey: 'studio.templates.name.twoToneSplit',
|
||||
kind: 'inner',
|
||||
archetype: 'cutout',
|
||||
tags: ['two-tone', 'colour-block', 'title', 'story', 'two-photos', 'cut-out', 'lively'],
|
||||
descr:
|
||||
'The left page a block of coral with the title in huge condensed capitals, the date on a band and a small tilted photo in a white border with a cut-out star; the right page a large photo, the story and a sand leaf.',
|
||||
background: '#2445b8',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 's1',
|
||||
frame: { x: -0.0143, y: -0.0143, w: 1.0143, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#ff7a59',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
role: 'title',
|
||||
on: 'accent',
|
||||
frame: { x: 0.0762, y: 0.0857, w: 0.8476, h: 0.4381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'shoulders',
|
||||
size: 0.5714,
|
||||
weight: 800,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 0.86,
|
||||
tracking: 0,
|
||||
color: '#2445b8',
|
||||
token: 'paper',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'top',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.5714,
|
||||
},
|
||||
{
|
||||
id: 't3',
|
||||
role: 'date',
|
||||
tint: { highlight: 'paper' },
|
||||
frame: { x: 0.0857, y: 0.5429, w: 0.5714, h: 0.0429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'oswald',
|
||||
size: 0.0429,
|
||||
weight: 500,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.45,
|
||||
color: '#fdf8ef',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
highlight: { color: '#2445b8', alpha: 1, padX: 0.0114, padY: 0.0052, radius: 0 },
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0429,
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:2',
|
||||
frame: { x: 0.4952, y: 0.6095, w: 0.4095, h: 0.3048 },
|
||||
rotation: -4,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'print',
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.019, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 's5',
|
||||
frame: { x: 0.4095, y: 0.8, w: 0.1429, h: 0.1381 },
|
||||
rotation: 12,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'print',
|
||||
kind: 'shape',
|
||||
shape: 'star-organic',
|
||||
fill: '#f1d9b5',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent2',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
role: 'photo:1',
|
||||
frame: { x: 1.0762, y: 0.0762, w: 0.8476, h: 0.5524 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 's7',
|
||||
frame: { x: 1.7429, y: 0.6667, w: 0.219, h: 0.2048 },
|
||||
rotation: 18,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'leaf-palm',
|
||||
fill: '#f1d9b5',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent2',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'body',
|
||||
frame: { x: 1.0762, y: 0.6762, w: 0.6286, h: 0.2571 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'inter',
|
||||
size: 0.0476,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.55,
|
||||
tracking: 0,
|
||||
color: '#fdf8ef',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'day-opener',
|
||||
labelKey: 'studio.templates.name.dayOpener',
|
||||
kind: 'inner',
|
||||
archetype: 'minimal-serif',
|
||||
tags: ['chapter', 'day', 'date', 'big-number', 'story', 'two-photos', 'calm'],
|
||||
descr:
|
||||
'A bleed photo on the left page; on the right the day of the month as a big numeral with the month under it, a hairline, the title in a large thin serif, the subtitle in spaced capitals, the story and a small photo.',
|
||||
background: null,
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: -0.0143, y: -0.0143, w: 1.0143, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'b2',
|
||||
role: 'date',
|
||||
frame: { x: 1.0762, y: 0.0857, w: 0.3048, h: 0.2952 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'head',
|
||||
font: 'cormorant',
|
||||
color: '#222222',
|
||||
accent: '#111111',
|
||||
textScale: 1,
|
||||
weight: 300,
|
||||
stale: false,
|
||||
token: 'ink',
|
||||
accentToken: 'ink',
|
||||
fontRole: 'display',
|
||||
kind: 'badge',
|
||||
variant: 'date',
|
||||
text: '14',
|
||||
sub: 'MAY',
|
||||
code: null,
|
||||
style: 'plain',
|
||||
autoColor: true,
|
||||
showIcon: true,
|
||||
showLabel: true,
|
||||
autoIconColor: true,
|
||||
iconColor: '#111111',
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
frame: { x: 1.0857, y: 0.4095, w: 0.8381, h: 0.0014 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'head',
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#222222',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'ink',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't4',
|
||||
role: 'title',
|
||||
frame: { x: 1.0857, y: 0.4476, w: 0.8381, h: 0.1429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'head',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'cormorant',
|
||||
size: 0.1905,
|
||||
weight: 300,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#222222',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1905,
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'subtitle',
|
||||
frame: { x: 1.0857, y: 0.6, w: 0.8381, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'head',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.4,
|
||||
color: '#646464',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'body',
|
||||
frame: { x: 1.0857, y: 0.6667, w: 0.5238, h: 0.2667 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'inter',
|
||||
size: 0.0452,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.5,
|
||||
tracking: 0,
|
||||
color: '#222222',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
},
|
||||
{
|
||||
id: 'p7',
|
||||
role: 'photo:2',
|
||||
frame: { x: 1.6476, y: 0.6667, w: 0.2762, h: 0.2667 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,718 @@
|
||||
import type { BookDecoratedTemplate } from '../template-format';
|
||||
|
||||
/**
|
||||
* Double spreads laid as a collage: polaroids, a row of stamps, a gallery
|
||||
* of arches. Each is drawn in the theme of its archetype.
|
||||
*
|
||||
* Drawn on a 210 mm page and kept as fractions of it (template-format.ts),
|
||||
* in the colours and faces of the theme named above; their links let any
|
||||
* other theme recolour them.
|
||||
*/
|
||||
export const INNER_COLLAGE: readonly BookDecoratedTemplate[] = [
|
||||
{
|
||||
id: 'polaroid-stack',
|
||||
labelKey: 'studio.templates.name.polaroidStack',
|
||||
kind: 'inner',
|
||||
archetype: 'stamp-collage',
|
||||
tags: ['polaroid', 'collage', 'tape', 'scrapbook', 'four-photos', 'lively'],
|
||||
descr:
|
||||
'Four photos as polaroids turned a few degrees and taped down, two on each page; on the right the title in a script, the date in spaced capitals and a few lines of the story.',
|
||||
background: '#fbf2e2',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.1048, y: 0.0952, w: 0.4571, h: 0.5429 },
|
||||
rotation: -4,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'polaroid',
|
||||
shadow: { dx: 0.0048, dy: 0.0057, color: '#2a1b2e', alpha: 0.16 },
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
role: 'photo:2',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.4857, y: 0.3143, w: 0.4286, h: 0.5048 },
|
||||
rotation: 5,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'polaroid',
|
||||
shadow: { dx: 0.0048, dy: 0.0057, color: '#2a1b2e', alpha: 0.16 },
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
role: 'photo:3',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 1.0952, y: 0.1048, w: 0.3905, h: 0.4667 },
|
||||
rotation: 3,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'polaroid',
|
||||
shadow: { dx: 0.0048, dy: 0.0057, color: '#2a1b2e', alpha: 0.16 },
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:4',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 1.5143, y: 0.1619, w: 0.4, h: 0.4762 },
|
||||
rotation: -6,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'polaroid',
|
||||
shadow: { dx: 0.0048, dy: 0.0057, color: '#2a1b2e', alpha: 0.16 },
|
||||
},
|
||||
{
|
||||
id: 's5',
|
||||
keep: true,
|
||||
frame: { x: 0.2476, y: 0.0571, w: 0.1524, h: 0.0762 },
|
||||
rotation: -8,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'maskingTape',
|
||||
colors: [],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 's6',
|
||||
tint: { colors: ['accent2', 'paper'] },
|
||||
frame: { x: 0.6286, y: 0.2762, w: 0.1714, h: 0.0476 },
|
||||
rotation: 12,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'washiStripes',
|
||||
colors: ['#e9801a', '#fbf2e2'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 's7',
|
||||
tint: { colors: ['accent3', 'paper'] },
|
||||
frame: { x: 1.2095, y: 0.0667, w: 0.1714, h: 0.0476 },
|
||||
rotation: -6,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'washiDots',
|
||||
colors: ['#12807f', '#fbf2e2'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 's8',
|
||||
keep: true,
|
||||
frame: { x: 1.6381, y: 0.1238, w: 0.1524, h: 0.0762 },
|
||||
rotation: 9,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'maskingTape',
|
||||
colors: [],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 't9',
|
||||
role: 'title',
|
||||
frame: { x: 1.1048, y: 0.6667, w: 0.819, h: 0.1429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'parisienne',
|
||||
size: 0.219,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#2a1b2e',
|
||||
token: 'ink',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'bottom',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.219,
|
||||
},
|
||||
{
|
||||
id: 't10',
|
||||
role: 'date',
|
||||
frame: { x: 1.1143, y: 0.8238, w: 0.5714, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.42,
|
||||
color: '#695c64',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 't11',
|
||||
role: 'body',
|
||||
frame: { x: 1.1143, y: 0.8667, w: 0.8095, h: 0.0762 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'serif',
|
||||
size: 0.0405,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.4,
|
||||
tracking: 0,
|
||||
color: '#2a1b2e',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
},
|
||||
{
|
||||
id: 's12',
|
||||
tint: { colors: ['accent2'] },
|
||||
frame: { x: 1.7905, y: 0.6952, w: 0.1048, h: 0.1048 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'sparkles',
|
||||
colors: ['#e9801a'],
|
||||
flipX: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'stamp-row',
|
||||
labelKey: 'studio.templates.name.stampRow',
|
||||
kind: 'inner',
|
||||
archetype: 'stamp-collage',
|
||||
tags: ['stamps', 'perforated', 'row', 'postmark', 'checker', 'four-photos', 'lively'],
|
||||
descr:
|
||||
'Four photos as perforated stamps in a loose row, two on each page, a postmark over the last; the title in a fat display serif on the left, the date and the story on the right, a checker strip along the foot.',
|
||||
background: '#fbf2e2',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 't1',
|
||||
role: 'title',
|
||||
frame: { x: 0.0952, y: 0.0857, w: 0.819, h: 0.1619 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'abril',
|
||||
size: 0.2095,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#2a1b2e',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.2095,
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
role: 'date',
|
||||
frame: { x: 1.1048, y: 0.1238, w: 0.819, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'right',
|
||||
leading: 1,
|
||||
tracking: 0.42,
|
||||
color: '#695c64',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
role: 'photo:1',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.1143, y: 0.3143, w: 0.381, h: 0.4571 },
|
||||
rotation: -3,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0143, color: '#ffffff' },
|
||||
shadow: { dx: 0.0057, dy: 0.0057, color: '#2a1b2e', alpha: 0.18 },
|
||||
edge: 'perforated',
|
||||
edgeSize: 0.0067,
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:2',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.5333, y: 0.2857, w: 0.381, h: 0.4571 },
|
||||
rotation: 2,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0143, color: '#ffffff' },
|
||||
shadow: { dx: 0.0057, dy: 0.0057, color: '#2a1b2e', alpha: 0.18 },
|
||||
edge: 'perforated',
|
||||
edgeSize: 0.0067,
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
role: 'photo:3',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 1.0857, y: 0.2952, w: 0.381, h: 0.4571 },
|
||||
rotation: -2,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0143, color: '#ffffff' },
|
||||
shadow: { dx: 0.0057, dy: 0.0057, color: '#2a1b2e', alpha: 0.18 },
|
||||
edge: 'perforated',
|
||||
edgeSize: 0.0067,
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
role: 'photo:4',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 1.5048, y: 0.2762, w: 0.381, h: 0.4571 },
|
||||
rotation: 3,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'stamp',
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0143, color: '#ffffff' },
|
||||
shadow: { dx: 0.0057, dy: 0.0057, color: '#2a1b2e', alpha: 0.18 },
|
||||
edge: 'perforated',
|
||||
edgeSize: 0.0067,
|
||||
},
|
||||
{
|
||||
id: 's7',
|
||||
tint: { colors: ['ink'] },
|
||||
frame: { x: 1.6667, y: 0.6095, w: 0.2571, h: 0.1476 },
|
||||
rotation: -12,
|
||||
opacity: 0.88,
|
||||
locked: false,
|
||||
group: 'stamp',
|
||||
kind: 'sticker',
|
||||
stickerId: 'postmark',
|
||||
colors: ['#2a1b2e'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'caption',
|
||||
frame: { x: 0.1143, y: 0.819, w: 0.8, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.3,
|
||||
color: '#695c64',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 't9',
|
||||
role: 'body',
|
||||
frame: { x: 1.1048, y: 0.8, w: 0.819, h: 0.1143 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'serif',
|
||||
size: 0.0429,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.55,
|
||||
tracking: 0,
|
||||
color: '#2a1b2e',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
},
|
||||
{
|
||||
id: 's10',
|
||||
tint: { fg: 'accent', bg: 'paper' },
|
||||
frame: { x: -0.0143, y: 0.9429, w: 2.0286, h: 0.0714 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#bd2275',
|
||||
gradient: 'none',
|
||||
paint: { type: 'pattern', pattern: 'checker', fg: '#bd2275', bg: '#fbf2e2', scale: 0.7, angle: 0 },
|
||||
stroke: null,
|
||||
token: 'accent',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'arch-gallery',
|
||||
labelKey: 'studio.templates.name.archGallery',
|
||||
kind: 'inner',
|
||||
archetype: 'postcard',
|
||||
tags: ['arches', 'gallery', 'mediterranean', 'six-photos', 'calm'],
|
||||
descr:
|
||||
'Six photos cut into arches with white borders, three on each page with the middle one raised; the title in a display serif and a script line at the head of the left page, the date and the story on the right.',
|
||||
background: '#e3f1e8',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 't1',
|
||||
role: 'title',
|
||||
frame: { x: 0.0952, y: 0.0762, w: 0.819, h: 0.1333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dmserif',
|
||||
size: 0.1905,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1905,
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
role: 'subtitle',
|
||||
frame: { x: 0.0952, y: 0.2095, w: 0.819, h: 0.0714 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dancing',
|
||||
size: 0.1238,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#c8453d',
|
||||
token: 'accent',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1238,
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.0952, y: 0.3333, w: 0.2476, h: 0.5524 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: 'arch',
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0105, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:2',
|
||||
frame: { x: 0.381, y: 0.2952, w: 0.2476, h: 0.5524 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: 'arch',
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0105, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
role: 'photo:3',
|
||||
frame: { x: 0.6667, y: 0.3333, w: 0.2476, h: 0.5524 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: 'arch',
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0105, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
role: 'photo:4',
|
||||
frame: { x: 1.0857, y: 0.1619, w: 0.2476, h: 0.5524 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: 'arch',
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0105, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 'p7',
|
||||
role: 'photo:5',
|
||||
frame: { x: 1.3714, y: 0.1238, w: 0.2476, h: 0.5524 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: 'arch',
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0105, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 'p8',
|
||||
role: 'photo:6',
|
||||
frame: { x: 1.6571, y: 0.1619, w: 0.2476, h: 0.5524 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: 'arch',
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0105, color: '#ffffff' },
|
||||
},
|
||||
{
|
||||
id: 't9',
|
||||
role: 'caption',
|
||||
frame: { x: 0.0952, y: 0.9143, w: 0.819, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.3,
|
||||
color: '#5a687c',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 't10',
|
||||
role: 'date',
|
||||
frame: { x: 1.0857, y: 0.0762, w: 0.819, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.42,
|
||||
color: '#5a687c',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 't11',
|
||||
role: 'body',
|
||||
frame: { x: 1.0857, y: 0.7524, w: 0.819, h: 0.181 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'inter',
|
||||
size: 0.0452,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.55,
|
||||
tracking: 0,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,805 @@
|
||||
import type { BookDecoratedTemplate } from '../template-format';
|
||||
|
||||
/**
|
||||
* Double spreads of travel ephemera: a map with ticket stubs, a filmstrip,
|
||||
* the back of a postcard. Each is drawn in the theme of its archetype.
|
||||
*
|
||||
* Drawn on a 210 mm page and kept as fractions of it (template-format.ts),
|
||||
* in the colours and faces of the theme named above; their links let any
|
||||
* other theme recolour them.
|
||||
*/
|
||||
export const INNER_EPHEMERA: readonly BookDecoratedTemplate[] = [
|
||||
{
|
||||
id: 'map-tickets',
|
||||
labelKey: 'studio.templates.name.mapTickets',
|
||||
kind: 'inner',
|
||||
archetype: 'grid-badge',
|
||||
tags: ['map', 'route', 'tickets', 'ephemera', 'two-photos', 'graphic'],
|
||||
descr:
|
||||
"The journey's map on the left page; on the right two ticket stubs, one with the title and the date, one turned with the subtitle, a boarding pass and two photos side by side under them.",
|
||||
background: null,
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'map',
|
||||
frame: { x: 0.0857, y: 0.0857, w: 0.8286, h: 0.8286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
font: 'sans',
|
||||
color: '#141414',
|
||||
accent: '#111111',
|
||||
textScale: 1,
|
||||
weight: 700,
|
||||
stale: false,
|
||||
token: 'ink',
|
||||
accentToken: 'accent',
|
||||
kind: 'map',
|
||||
source: 'vector',
|
||||
tileUrl: '',
|
||||
attribution: '',
|
||||
zoom: null,
|
||||
style: 'minimal',
|
||||
showLand: true,
|
||||
showRoute: true,
|
||||
showPins: true,
|
||||
showLabels: false,
|
||||
routeStyle: 'plain',
|
||||
routeArc: 'straight',
|
||||
routeDash: 'solid',
|
||||
pinStyle: 'dot',
|
||||
countries: [],
|
||||
points: [],
|
||||
path: [],
|
||||
roads: [],
|
||||
fitPadding: 0.4,
|
||||
fitToCountries: true,
|
||||
clip: 'rect',
|
||||
tripId: null,
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
frame: { x: 0.0857, y: 0.0857, w: 0.8286, h: 0.8286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: null,
|
||||
gradient: 'none',
|
||||
stroke: '#141414',
|
||||
strokeToken: 'ink',
|
||||
strokeWidth: 0.0014,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
frame: { x: 1.0857, y: 0.1143, w: 0.581, h: 0.2619 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'ticket',
|
||||
kind: 'shape',
|
||||
shape: 'ticket-stub',
|
||||
fill: '#f2eee7',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent2',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't4',
|
||||
role: 'title',
|
||||
on: 'accent2',
|
||||
frame: { x: 1.1429, y: 0.1571, w: 0.4476, h: 0.1048 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'ticket',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.1238,
|
||||
weight: 800,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#141414',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1238,
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'date',
|
||||
frame: { x: 1.0857, y: 0.3905, w: 0.1429, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'ticket',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0357,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.4,
|
||||
color: '#141414',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0357,
|
||||
},
|
||||
{
|
||||
id: 's6',
|
||||
frame: { x: 1.2476, y: 0.419, w: 0.581, h: 0.2619 },
|
||||
rotation: 4,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'stub',
|
||||
kind: 'shape',
|
||||
shape: 'ticket-stub',
|
||||
fill: null,
|
||||
gradient: 'none',
|
||||
stroke: '#141414',
|
||||
strokeToken: 'ink',
|
||||
strokeWidth: 0.0019,
|
||||
strokeStyle: 'dashed',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'subtitle',
|
||||
keep: true,
|
||||
frame: { x: 1.3143, y: 0.5048, w: 0.4476, h: 0.0667 },
|
||||
rotation: 4,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'stub',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0476,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.3,
|
||||
color: '#141414',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0476,
|
||||
},
|
||||
{
|
||||
id: 's8',
|
||||
keep: true,
|
||||
frame: { x: 1.6762, y: 0.0952, w: 0.2571, h: 0.1333 },
|
||||
rotation: 8,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'sticker',
|
||||
stickerId: 'boardingPass',
|
||||
colors: [],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 'p9',
|
||||
role: 'photo:1',
|
||||
frame: { x: 1.0857, y: 0.7333, w: 0.4095, h: 0.2 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p10',
|
||||
role: 'photo:2',
|
||||
frame: { x: 1.5143, y: 0.7333, w: 0.4095, h: 0.2 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'filmstrip-script',
|
||||
labelKey: 'studio.templates.name.filmstripScript',
|
||||
kind: 'inner',
|
||||
archetype: 'grid-badge',
|
||||
tags: ['filmstrip', 'six-photos', 'script', 'story', 'cinema', 'graphic'],
|
||||
descr:
|
||||
'A black filmstrip across the spread with sprocket holes along both edges and six photos as its frames, three on each page; under it the title in a script, the date, the caption and the story.',
|
||||
background: null,
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 's1',
|
||||
frame: { x: -0.0143, y: 0.1905, w: 2.0286, h: 0.4571 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#141414',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'ink',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 'l2',
|
||||
tint: { stroke: 'paper' },
|
||||
frame: { x: -0.0143, y: 0.2119, w: 2.0286, h: 0.0143 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'line',
|
||||
points: [
|
||||
{ x: 0, y: 0.5 },
|
||||
{ x: 1, y: 0.5 },
|
||||
],
|
||||
curve: 'straight',
|
||||
stroke: '#ffffff',
|
||||
strokeWidth: 0.0086,
|
||||
dash: 'dashed',
|
||||
squiggle: 'none',
|
||||
startCap: 'none',
|
||||
endCap: 'none',
|
||||
wobble: 0,
|
||||
ornament: 'none',
|
||||
},
|
||||
{
|
||||
id: 'l3',
|
||||
tint: { stroke: 'paper' },
|
||||
frame: { x: -0.0143, y: 0.6119, w: 2.0286, h: 0.0143 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'line',
|
||||
points: [
|
||||
{ x: 0, y: 0.5 },
|
||||
{ x: 1, y: 0.5 },
|
||||
],
|
||||
curve: 'straight',
|
||||
stroke: '#ffffff',
|
||||
strokeWidth: 0.0086,
|
||||
dash: 'dashed',
|
||||
squiggle: 'none',
|
||||
startCap: 'none',
|
||||
endCap: 'none',
|
||||
wobble: 0,
|
||||
ornament: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.0762, y: 0.2476, w: 0.2667, h: 0.3429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
role: 'photo:2',
|
||||
frame: { x: 0.3667, y: 0.2476, w: 0.2667, h: 0.3429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
role: 'photo:3',
|
||||
frame: { x: 0.6571, y: 0.2476, w: 0.2667, h: 0.3429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p7',
|
||||
role: 'photo:4',
|
||||
frame: { x: 1.0762, y: 0.2476, w: 0.2667, h: 0.3429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p8',
|
||||
role: 'photo:5',
|
||||
frame: { x: 1.3667, y: 0.2476, w: 0.2667, h: 0.3429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p9',
|
||||
role: 'photo:6',
|
||||
frame: { x: 1.6571, y: 0.2476, w: 0.2667, h: 0.3429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 't10',
|
||||
role: 'caption',
|
||||
frame: { x: 0.0762, y: 0.6952, w: 0.8476, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'inter',
|
||||
size: 0.0381,
|
||||
weight: 400,
|
||||
italic: true,
|
||||
align: 'left',
|
||||
leading: 1.2,
|
||||
tracking: 0,
|
||||
color: '#5b5b5b',
|
||||
token: 'muted',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 't11',
|
||||
role: 'body',
|
||||
frame: { x: 0.0762, y: 0.7619, w: 0.8476, h: 0.1714 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'inter',
|
||||
size: 0.0452,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.55,
|
||||
tracking: 0,
|
||||
color: '#141414',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
},
|
||||
{
|
||||
id: 't12',
|
||||
role: 'title',
|
||||
frame: { x: 1.0762, y: 0.6857, w: 0.8476, h: 0.181 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'allura',
|
||||
size: 0.2857,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'right',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#141414',
|
||||
token: 'ink',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.2857,
|
||||
},
|
||||
{
|
||||
id: 't13',
|
||||
role: 'date',
|
||||
frame: { x: 1.0762, y: 0.8857, w: 0.8476, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'right',
|
||||
leading: 1,
|
||||
tracking: 0.4,
|
||||
color: '#5b5b5b',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'postcard-back',
|
||||
labelKey: 'studio.templates.name.postcardBack',
|
||||
kind: 'inner',
|
||||
archetype: 'postcard',
|
||||
tags: ['postcard', 'handwriting', 'stamp', 'postmark', 'story', 'two-photos', 'calm'],
|
||||
descr:
|
||||
'A bleed photo on the left page as the front of a postcard; on the right the back of the card: the title and the story in handwriting on the left half, a stamp with a postmark, the place and the date on the address lines.',
|
||||
background: '#f3d58a',
|
||||
backgroundToken: 'accent3',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: -0.0143, y: -0.0143, w: 1.0143, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
frame: { x: 1.0905, y: 0.119, w: 0.8381, h: 0.7905 },
|
||||
rotation: 0,
|
||||
opacity: 0.12,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#1f2d4d',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'ink',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0.0071,
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
frame: { x: 1.0762, y: 0.1048, w: 0.8381, h: 0.7905 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#e3f1e8',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'paper',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0.0071,
|
||||
},
|
||||
{
|
||||
id: 'l4',
|
||||
tint: { stroke: 'ink' },
|
||||
frame: { x: 1.4905, y: 0.1619, w: 0.0095, h: 0.6762 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'line',
|
||||
points: [
|
||||
{ x: 0.5, y: 0 },
|
||||
{ x: 0.5, y: 1 },
|
||||
],
|
||||
curve: 'straight',
|
||||
stroke: '#1f2d4d',
|
||||
strokeWidth: 0.0014,
|
||||
dash: 'solid',
|
||||
squiggle: 'none',
|
||||
startCap: 'none',
|
||||
endCap: 'none',
|
||||
wobble: 0,
|
||||
ornament: 'none',
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'title',
|
||||
frame: { x: 1.1238, y: 0.1524, w: 0.3429, h: 0.1048 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dancing',
|
||||
size: 0.1238,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1238,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'body',
|
||||
frame: { x: 1.1238, y: 0.2762, w: 0.3429, h: 0.5714 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dancing',
|
||||
size: 0.0571,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.35,
|
||||
tracking: 0,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
},
|
||||
{
|
||||
id: 'p7',
|
||||
role: 'photo:2',
|
||||
frame: { x: 1.7333, y: 0.1429, w: 0.1429, h: 0.1714 },
|
||||
rotation: 3,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'stamp',
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0076, color: '#ffffff' },
|
||||
edge: 'perforated',
|
||||
edgeSize: 0.0052,
|
||||
},
|
||||
{
|
||||
id: 's8',
|
||||
tint: { colors: ['ink'] },
|
||||
frame: { x: 1.5714, y: 0.1905, w: 0.2, h: 0.1143 },
|
||||
rotation: -10,
|
||||
opacity: 0.8,
|
||||
locked: false,
|
||||
group: 'stamp',
|
||||
kind: 'sticker',
|
||||
stickerId: 'postmark',
|
||||
colors: ['#1f2d4d'],
|
||||
flipX: false,
|
||||
},
|
||||
{
|
||||
id: 't9',
|
||||
role: 'subtitle',
|
||||
frame: { x: 1.5333, y: 0.5238, w: 0.3429, h: 0.0429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'address',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dancing',
|
||||
size: 0.0667,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'bottom',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0667,
|
||||
},
|
||||
{
|
||||
id: 'l10',
|
||||
tint: { stroke: 'ink' },
|
||||
frame: { x: 1.5333, y: 0.5667, w: 0.3429, h: 0.0095 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'address',
|
||||
kind: 'line',
|
||||
points: [
|
||||
{ x: 0, y: 0.5 },
|
||||
{ x: 1, y: 0.5 },
|
||||
],
|
||||
curve: 'straight',
|
||||
stroke: '#1f2d4d',
|
||||
strokeWidth: 0.0012,
|
||||
dash: 'solid',
|
||||
squiggle: 'none',
|
||||
startCap: 'none',
|
||||
endCap: 'none',
|
||||
wobble: 0,
|
||||
ornament: 'none',
|
||||
},
|
||||
{
|
||||
id: 't11',
|
||||
role: 'date',
|
||||
frame: { x: 1.5333, y: 0.6, w: 0.3429, h: 0.0429 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'address',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dancing',
|
||||
size: 0.0667,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'bottom',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0667,
|
||||
},
|
||||
{
|
||||
id: 'l12',
|
||||
tint: { stroke: 'ink' },
|
||||
frame: { x: 1.5333, y: 0.6429, w: 0.3429, h: 0.0095 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'address',
|
||||
kind: 'line',
|
||||
points: [
|
||||
{ x: 0, y: 0.5 },
|
||||
{ x: 1, y: 0.5 },
|
||||
],
|
||||
curve: 'straight',
|
||||
stroke: '#1f2d4d',
|
||||
strokeWidth: 0.0012,
|
||||
dash: 'solid',
|
||||
squiggle: 'none',
|
||||
startCap: 'none',
|
||||
endCap: 'none',
|
||||
wobble: 0,
|
||||
ornament: 'none',
|
||||
},
|
||||
{
|
||||
id: 'l13',
|
||||
tint: { stroke: 'ink' },
|
||||
frame: { x: 1.5333, y: 0.719, w: 0.3429, h: 0.0095 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'address',
|
||||
kind: 'line',
|
||||
points: [
|
||||
{ x: 0, y: 0.5 },
|
||||
{ x: 1, y: 0.5 },
|
||||
],
|
||||
curve: 'straight',
|
||||
stroke: '#1f2d4d',
|
||||
strokeWidth: 0.0012,
|
||||
dash: 'solid',
|
||||
squiggle: 'none',
|
||||
startCap: 'none',
|
||||
endCap: 'none',
|
||||
wobble: 0,
|
||||
ornament: 'none',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,718 @@
|
||||
import type { BookDecoratedTemplate } from '../template-format';
|
||||
|
||||
/**
|
||||
* Double spreads of photo grids, their gutters 3, 5 and 6 mm wide. Each is
|
||||
* drawn in the theme of its archetype.
|
||||
*
|
||||
* Drawn on a 210 mm page and kept as fractions of it (template-format.ts),
|
||||
* in the colours and faces of the theme named above; their links let any
|
||||
* other theme recolour them.
|
||||
*/
|
||||
export const INNER_GRIDS: readonly BookDecoratedTemplate[] = [
|
||||
{
|
||||
id: 'grid-tight',
|
||||
labelKey: 'studio.templates.name.gridTight',
|
||||
kind: 'inner',
|
||||
archetype: 'grid-badge',
|
||||
tags: ['grid', 'eight-photos', 'full-bleed', 'contact-sheet', 'gutter-3mm', 'graphic'],
|
||||
descr:
|
||||
'Eight photos, four on each page two by two, cut to the bleed with 3 mm white gutters and a clear gutter at the fold; the title on a small white tab at the foot of the right page.',
|
||||
background: null,
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: -0.0143, y: -0.0143, w: 0.5036, h: 0.5071 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
role: 'photo:2',
|
||||
frame: { x: 0.5036, y: -0.0143, w: 0.4893, h: 0.5071 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
role: 'photo:3',
|
||||
frame: { x: 1.0071, y: -0.0143, w: 0.4893, h: 0.5071 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:4',
|
||||
frame: { x: 1.5107, y: -0.0143, w: 0.5036, h: 0.5071 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
role: 'photo:5',
|
||||
frame: { x: -0.0143, y: 0.5071, w: 0.5036, h: 0.5071 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
role: 'photo:6',
|
||||
frame: { x: 0.5036, y: 0.5071, w: 0.4893, h: 0.5071 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p7',
|
||||
role: 'photo:7',
|
||||
frame: { x: 1.0071, y: 0.5071, w: 0.4893, h: 0.5071 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p8',
|
||||
role: 'photo:8',
|
||||
frame: { x: 1.5107, y: 0.5071, w: 0.5036, h: 0.5071 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 's9',
|
||||
frame: { x: 1.5905, y: 0.8857, w: 0.3333, h: 0.0667 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'tab',
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#ffffff',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'paper',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't10',
|
||||
role: 'title',
|
||||
frame: { x: 1.6095, y: 0.8952, w: 0.2952, h: 0.0476 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'tab',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0429,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.4,
|
||||
color: '#141414',
|
||||
token: 'ink',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0429,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'grid-feature',
|
||||
labelKey: 'studio.templates.name.gridFeature',
|
||||
kind: 'inner',
|
||||
archetype: 'minimal-serif',
|
||||
tags: ['grid', 'five-photos', 'feature', 'margins', 'gutter-5mm', 'calm'],
|
||||
descr:
|
||||
'One large photo on the left page over the title in a thin serif and the date; four photos two by two on the right page, all inside even margins with 5 mm gutters.',
|
||||
background: null,
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.0762, y: 0.0762, w: 0.8476, h: 0.7143 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
role: 'title',
|
||||
frame: { x: 0.0762, y: 0.819, w: 0.5714, h: 0.1143 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'cormorant',
|
||||
size: 0.1619,
|
||||
weight: 300,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#222222',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1619,
|
||||
},
|
||||
{
|
||||
id: 't3',
|
||||
role: 'date',
|
||||
frame: { x: 0.6667, y: 0.8571, w: 0.2571, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'right',
|
||||
leading: 1,
|
||||
tracking: 0.4,
|
||||
color: '#646464',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:2',
|
||||
frame: { x: 1.0762, y: 0.0762, w: 0.4119, h: 0.4119 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
role: 'photo:3',
|
||||
frame: { x: 1.5119, y: 0.0762, w: 0.4119, h: 0.4119 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
role: 'photo:4',
|
||||
frame: { x: 1.0762, y: 0.5119, w: 0.4119, h: 0.4119 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p7',
|
||||
role: 'photo:5',
|
||||
frame: { x: 1.5119, y: 0.5119, w: 0.4119, h: 0.4119 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'grid-columns',
|
||||
labelKey: 'studio.templates.name.gridColumns',
|
||||
kind: 'inner',
|
||||
archetype: 'spine-band',
|
||||
tags: ['grid', 'five-photos', 'columns', 'story', 'gutter-6mm', 'calm'],
|
||||
descr:
|
||||
'A tall photo beside two stacked ones on the left page, 6 mm apart; a wide photo on the right page over the title in a serif, the date in spaced capitals and the story, inside even margins.',
|
||||
background: '#efe8db',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: 0.0762, y: 0.0762, w: 0.4571, h: 0.8476 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
role: 'photo:2',
|
||||
frame: { x: 0.5619, y: 0.0762, w: 0.3619, h: 0.4095 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
role: 'photo:3',
|
||||
frame: { x: 0.5619, y: 0.5143, w: 0.3619, h: 0.4095 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:4',
|
||||
frame: { x: 1.0762, y: 0.0762, w: 0.8476, h: 0.4857 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 's5',
|
||||
frame: { x: 1.0762, y: 0.6, w: 0.1429, h: 0.0024 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#b0603c',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent2',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'title',
|
||||
frame: { x: 1.0762, y: 0.6286, w: 0.8476, h: 0.1143 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'cormorant',
|
||||
size: 0.1524,
|
||||
weight: 500,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#2e2a26',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1524,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'date',
|
||||
frame: { x: 1.0762, y: 0.7524, w: 0.8476, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'title',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0381,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.4,
|
||||
color: '#68635c',
|
||||
token: 'muted',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0381,
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'body',
|
||||
frame: { x: 1.0762, y: 0.8095, w: 0.8476, h: 0.1238 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'garamond',
|
||||
size: 0.0452,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.5,
|
||||
tracking: 0,
|
||||
color: '#2e2a26',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'grid-mosaic',
|
||||
labelKey: 'studio.templates.name.gridMosaic',
|
||||
kind: 'inner',
|
||||
archetype: 'poster-pop',
|
||||
tags: ['grid', 'mosaic', 'six-photos', 'colour-tile', 'full-bleed', 'gutter-5mm', 'lively'],
|
||||
descr:
|
||||
'A mosaic of six photos cut to the bleed with 5 mm gutters and a coral tile among them that holds the title in heavy capitals and the date; a wide photo across the head of the right page.',
|
||||
background: '#fbf1de',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: -0.0143, y: -0.0143, w: 0.6095, h: 0.6095 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
frame: { x: 0.619, y: -0.0143, w: 0.369, h: 0.6095 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'tile',
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#ef4e45',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'accent',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't3',
|
||||
role: 'title',
|
||||
on: 'accent',
|
||||
frame: { x: 0.6476, y: 0.0667, w: 0.3119, h: 0.4 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'tile',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'archivo',
|
||||
size: 0.1905,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 0.9,
|
||||
tracking: 0,
|
||||
color: '#fbf1de',
|
||||
token: 'paper',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'top',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1905,
|
||||
},
|
||||
{
|
||||
id: 't4',
|
||||
role: 'date',
|
||||
on: 'accent',
|
||||
frame: { x: 0.6476, y: 0.4762, w: 0.3119, h: 0.0667 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'tile',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'anton',
|
||||
size: 0.1238,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#fbf1de',
|
||||
token: 'paper',
|
||||
fontRole: 'heading',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1238,
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
role: 'photo:2',
|
||||
frame: { x: -0.0143, y: 0.619, w: 0.6095, h: 0.3952 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
role: 'photo:3',
|
||||
frame: { x: 0.619, y: 0.619, w: 0.369, h: 0.3952 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p7',
|
||||
role: 'photo:4',
|
||||
frame: { x: 1.0119, y: -0.0143, w: 1.0024, h: 0.6095 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p8',
|
||||
role: 'photo:5',
|
||||
frame: { x: 1.0119, y: 0.619, w: 0.4881, h: 0.3952 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p9',
|
||||
role: 'photo:6',
|
||||
frame: { x: 1.5238, y: 0.619, w: 0.4905, h: 0.3952 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,533 @@
|
||||
import type { BookDecoratedTemplate } from '../template-format';
|
||||
|
||||
/**
|
||||
* Double spreads for a story: a hero photo with the words beside it. Each
|
||||
* is drawn in the theme of its archetype.
|
||||
*
|
||||
* Drawn on a 210 mm page and kept as fractions of it (template-format.ts),
|
||||
* in the colours and faces of the theme named above; their links let any
|
||||
* other theme recolour them.
|
||||
*/
|
||||
export const INNER_STORY: readonly BookDecoratedTemplate[] = [
|
||||
{
|
||||
id: 'hero-script-word',
|
||||
labelKey: 'studio.templates.name.heroScriptWord',
|
||||
kind: 'inner',
|
||||
archetype: 'postcard',
|
||||
tags: ['hero', 'script', 'overlay', 'story', 'two-photos', 'lively'],
|
||||
descr:
|
||||
'A bleed hero photo on the left page with the title laid over it in a giant script; on the right the date, the subtitle and the story in a column beside a tall second photo with its caption.',
|
||||
background: '#e3f1e8',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: -0.0143, y: -0.0143, w: 1.0143, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
frame: { x: -0.0143, y: 0.5333, w: 1.0143, h: 0.481 },
|
||||
rotation: 0,
|
||||
opacity: 0.55,
|
||||
locked: false,
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#1f2d4d',
|
||||
gradient: 'down',
|
||||
stroke: null,
|
||||
token: 'ink',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't3',
|
||||
role: 'title',
|
||||
tint: { shadow: 'ink' },
|
||||
frame: { x: 0.0667, y: 0.4952, w: 0.8762, h: 0.381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dancing',
|
||||
size: 0.4571,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#e3f1e8',
|
||||
token: 'paper',
|
||||
fontRole: 'script',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'bottom',
|
||||
shadow: { dx: 0.0033, dy: 0.0033, color: '#1f2d4d', alpha: 0.35 },
|
||||
fit: 'shrink',
|
||||
fitMax: 0.4571,
|
||||
},
|
||||
{
|
||||
id: 't4',
|
||||
role: 'date',
|
||||
frame: { x: 1.0857, y: 0.1143, w: 0.8381, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'head',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0405,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.42,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0405,
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'subtitle',
|
||||
frame: { x: 1.0857, y: 0.1619, w: 0.8381, h: 0.0762 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'head',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'dmserif',
|
||||
size: 0.1048,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.05,
|
||||
tracking: 0,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1048,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'body',
|
||||
frame: { x: 1.0857, y: 0.2857, w: 0.4952, h: 0.6381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'inter',
|
||||
size: 0.0476,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.55,
|
||||
tracking: 0,
|
||||
color: '#1f2d4d',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
},
|
||||
{
|
||||
id: 'p7',
|
||||
role: 'photo:2',
|
||||
frame: { x: 1.6286, y: 0.2857, w: 0.2952, h: 0.581 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'picture',
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 't8',
|
||||
role: 'caption',
|
||||
frame: { x: 1.6286, y: 0.8857, w: 0.2952, h: 0.0476 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'picture',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'inter',
|
||||
size: 0.0357,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.35,
|
||||
tracking: 0.04,
|
||||
color: '#5a687c',
|
||||
token: 'muted',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0357,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'hero-trio-wordart',
|
||||
labelKey: 'studio.templates.name.heroTrioWordart',
|
||||
kind: 'inner',
|
||||
archetype: 'poster-pop',
|
||||
tags: ['hero', 'word-art', 'photo-filled', 'three-grid', 'story', 'four-photos', 'loud'],
|
||||
descr:
|
||||
'A bleed hero photo on the left page; on the right the title in heavy capitals filled with the hero photo, three photos in a row with 2 mm gutters, the date and the story under them.',
|
||||
background: '#fbf1de',
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
frame: { x: -0.0143, y: -0.0143, w: 1.0143, h: 1.0286 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
role: 'title',
|
||||
fillPhoto: 'photo:1',
|
||||
frame: { x: 1.0762, y: 0.0762, w: 0.8476, h: 0.2571 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'archivo',
|
||||
size: 0.3714,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 0.9,
|
||||
tracking: 0,
|
||||
color: '#ef4e45',
|
||||
token: 'accent',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.3714,
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
role: 'photo:2',
|
||||
frame: { x: 1.0762, y: 0.3714, w: 0.2762, h: 0.3524 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
role: 'photo:3',
|
||||
frame: { x: 1.3619, y: 0.3714, w: 0.2762, h: 0.3524 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
role: 'photo:4',
|
||||
frame: { x: 1.6476, y: 0.3714, w: 0.2762, h: 0.3524 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
role: 'date',
|
||||
frame: { x: 1.0762, y: 0.7619, w: 0.8476, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0405,
|
||||
weight: 600,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.42,
|
||||
color: '#1d1a2f',
|
||||
token: 'ink',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0405,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'body',
|
||||
frame: { x: 1.0762, y: 0.819, w: 0.8476, h: 0.1238 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'inter',
|
||||
size: 0.0452,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.5,
|
||||
tracking: 0,
|
||||
color: '#1d1a2f',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'story-quote',
|
||||
labelKey: 'studio.templates.name.storyQuote',
|
||||
kind: 'inner',
|
||||
archetype: 'minimal-serif',
|
||||
tags: ['story', 'quote', 'serif', 'calm', 'one-photo', 'text'],
|
||||
descr:
|
||||
'A photo with a hairline border on the left page with its caption; on the right a large opening quotation mark, the title set as a quote in a thin italic serif, the date, a short rule and the story.',
|
||||
background: null,
|
||||
backgroundToken: 'paper',
|
||||
elements: [
|
||||
{
|
||||
id: 'p1',
|
||||
role: 'photo:1',
|
||||
tint: { border: 'ink' },
|
||||
frame: { x: 0.0857, y: 0.0857, w: 0.8286, h: 0.819 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'picture',
|
||||
kind: 'photo',
|
||||
photoId: null,
|
||||
fit: 'cover',
|
||||
focalX: 0.5,
|
||||
focalY: 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
border: { width: 0.0014, color: '#222222' },
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
role: 'caption',
|
||||
frame: { x: 0.0857, y: 0.9238, w: 0.8286, h: 0.0381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'picture',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'inter',
|
||||
size: 0.0357,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.35,
|
||||
tracking: 0.04,
|
||||
color: '#646464',
|
||||
token: 'muted',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0357,
|
||||
},
|
||||
{
|
||||
id: 't3',
|
||||
frame: { x: 1.0762, y: 0.0571, w: 0.1905, h: 0.2381 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'quote',
|
||||
kind: 'text',
|
||||
text: '“',
|
||||
font: 'cormorant',
|
||||
size: 0.6667,
|
||||
weight: 300,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0,
|
||||
color: '#857d72',
|
||||
token: 'accent',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
},
|
||||
{
|
||||
id: 't4',
|
||||
role: 'title',
|
||||
frame: { x: 1.0857, y: 0.3048, w: 0.8381, h: 0.1905 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'quote',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'cormorant',
|
||||
size: 0.1429,
|
||||
weight: 300,
|
||||
italic: true,
|
||||
align: 'left',
|
||||
leading: 1.1,
|
||||
tracking: 0,
|
||||
color: '#222222',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.1429,
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
role: 'date',
|
||||
frame: { x: 1.0857, y: 0.5143, w: 0.8381, h: 0.0333 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'quote',
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'montserrat',
|
||||
size: 0.0405,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1,
|
||||
tracking: 0.42,
|
||||
color: '#646464',
|
||||
token: 'muted',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
transform: 'upper',
|
||||
vAlign: 'middle',
|
||||
fit: 'shrink',
|
||||
fitMax: 0.0405,
|
||||
},
|
||||
{
|
||||
id: 's6',
|
||||
frame: { x: 1.0857, y: 0.5714, w: 0.1143, h: 0.0019 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
group: 'quote',
|
||||
kind: 'shape',
|
||||
shape: 'rect',
|
||||
fill: '#222222',
|
||||
gradient: 'none',
|
||||
stroke: null,
|
||||
token: 'ink',
|
||||
strokeWidth: 0,
|
||||
strokeStyle: 'solid',
|
||||
radius: 0,
|
||||
},
|
||||
{
|
||||
id: 't7',
|
||||
role: 'body',
|
||||
frame: { x: 1.0857, y: 0.6095, w: 0.8381, h: 0.3238 },
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
locked: false,
|
||||
kind: 'text',
|
||||
text: '',
|
||||
font: 'inter',
|
||||
size: 0.0476,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
align: 'left',
|
||||
leading: 1.55,
|
||||
tracking: 0,
|
||||
color: '#222222',
|
||||
token: 'ink',
|
||||
fontRole: 'body',
|
||||
binding: null,
|
||||
overridden: false,
|
||||
vAlign: 'top',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,478 @@
|
||||
import { contrastRatio, requiredContrast } from '../book-colour';
|
||||
import { designFindings } from '../book-findings-design';
|
||||
import { applyBookTemplate, findBookTemplate } from '../book-layouts';
|
||||
import { bookPageSetupForPreset } from '../book-pages';
|
||||
import { fitSpreadTexts } from '../book-text-fit';
|
||||
import {
|
||||
bookBadgeElementSchema,
|
||||
bookLineElementSchema,
|
||||
bookMapElementSchema,
|
||||
bookPhotoElementSchema,
|
||||
bookShapeElementSchema,
|
||||
bookSpreadSchema,
|
||||
bookStickerElementSchema,
|
||||
bookTextElementSchema,
|
||||
type BookElement,
|
||||
type BookPageSetup,
|
||||
type BookSpread,
|
||||
type BookTextElement,
|
||||
} from '../book.schema';
|
||||
import { bookThemeOf } from '../theme/book-theme';
|
||||
import type { BookTheme } from '../theme/book-theme.schema';
|
||||
import { applyDecoratedTemplate } from './template-apply';
|
||||
import { BOOK_DECORATED_TEMPLATES, findDecoratedTemplate } from './template-catalogue';
|
||||
import type { BookDecoratedTemplate, BookTemplateElement } from './template-format';
|
||||
import { bookTemplateWords } from './template-words';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
// TPLAPPLY-001 to TPLAPPLY-021
|
||||
|
||||
/**
|
||||
* Laying a page out on a decorated template: the page's photos poured into
|
||||
* the photo roles in order, its words into the word roles and the book's
|
||||
* words into the roles it has none for, the colours and faces taken from the
|
||||
* book's theme, every measure scaled to the book's page, and what has no room
|
||||
* parked with the page.
|
||||
*/
|
||||
|
||||
const square = bookPageSetupForPreset('square-210');
|
||||
const landscape = bookPageSetupForPreset('a4-landscape');
|
||||
const big = bookPageSetupForPreset('square-300');
|
||||
|
||||
let seq = 0;
|
||||
const newId = (p: string) => `${p}-${++seq}`;
|
||||
|
||||
const tpl = (id: string) => {
|
||||
const found = findDecoratedTemplate(id);
|
||||
if (!found) throw new Error(`no template ${id}`);
|
||||
return found;
|
||||
};
|
||||
|
||||
const photo = (id: string, photoId: number, over: Partial<BookElement> = {}) =>
|
||||
bookPhotoElementSchema.parse({ id, kind: 'photo', frame: { x: 10, y: 10, w: 40, h: 30 }, photoId, ...over });
|
||||
const text = (id: string, words: string, over: Partial<BookTextElement> = {}) =>
|
||||
bookTextElementSchema.parse({ id, kind: 'text', frame: { x: 10, y: 60, w: 120, h: 30 }, text: words, ...over });
|
||||
|
||||
function page(role: BookSpread['role'], elements: BookElement[], over: Partial<BookSpread> = {}): BookSpread {
|
||||
return bookSpreadSchema.parse({ id: 'sp', role, elements, ...over });
|
||||
}
|
||||
|
||||
const lay = (
|
||||
spread: BookSpread,
|
||||
id: string | BookDecoratedTemplate,
|
||||
setup: BookPageSetup = square,
|
||||
opts: Parameters<typeof applyDecoratedTemplate>[3] = {},
|
||||
) => applyDecoratedTemplate(spread, typeof id === 'string' ? tpl(id) : id, setup, { newId, locale: 'en', ...opts });
|
||||
|
||||
const photosOf = (spread: BookSpread) => spread.elements.filter((e) => e.kind === 'photo');
|
||||
const textOf = (spread: BookSpread, words: string) =>
|
||||
spread.elements.find((e): e is BookTextElement => e.kind === 'text' && e.text === words);
|
||||
|
||||
/** A small template drawn for the specs, in fractions as the format keeps them. */
|
||||
function synthetic(): BookDecoratedTemplate {
|
||||
const base = { frame: { x: 0, y: 0, w: 1, h: 1 } };
|
||||
const els: BookTemplateElement[] = [
|
||||
{
|
||||
...bookPhotoElementSchema.parse({ id: 'p1', kind: 'photo', ...base }),
|
||||
frame: { x: -0.0143, y: -0.0143, w: 0.5143, h: 1.0286 },
|
||||
border: { width: 3 / 210, color: '#ffffff' },
|
||||
role: 'photo:1',
|
||||
tint: { border: 'accent' },
|
||||
},
|
||||
{
|
||||
...bookShapeElementSchema.parse({ id: 's2', kind: 'shape', shape: 'ellipse', ...base }),
|
||||
frame: { x: 0.6, y: 0.1, w: 0.2, h: 0.2 },
|
||||
fill: '#123456',
|
||||
token: 'accent',
|
||||
group: 'g1',
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
...bookTextElementSchema.parse({ id: 't3', kind: 'text', ...base }),
|
||||
frame: { x: 0.6, y: 0.1, w: 0.2, h: 0.2 },
|
||||
size: 24 / 210,
|
||||
fit: 'shrink',
|
||||
fitMax: 24 / 210,
|
||||
color: '#222222',
|
||||
token: 'ink',
|
||||
fontRole: 'display',
|
||||
group: 'g1',
|
||||
role: 'title',
|
||||
},
|
||||
{
|
||||
...bookStickerElementSchema.parse({ id: 'k4', kind: 'sticker', stickerId: 'lemon', ...base }),
|
||||
frame: { x: 0.6, y: 0.6, w: 0.2, h: 0.16 },
|
||||
colors: ['#111111', '#222222'],
|
||||
tint: { colors: ['accent', null] },
|
||||
},
|
||||
{
|
||||
...bookLineElementSchema.parse({ id: 'l5', kind: 'line', ...base }),
|
||||
frame: { x: 0.55, y: 0.9, w: 0.4, h: 0.01 },
|
||||
strokeWidth: 0.6 / 210,
|
||||
tint: { stroke: 'accent2' },
|
||||
},
|
||||
];
|
||||
return {
|
||||
id: 'test-synthetic',
|
||||
labelKey: 'studio.templates.name.testSynthetic',
|
||||
kind: 'single',
|
||||
archetype: 'postcard',
|
||||
tags: ['test'],
|
||||
descr: 'A small template drawn for the specs.',
|
||||
background: '#fafafa',
|
||||
backgroundToken: 'paper',
|
||||
elements: els,
|
||||
};
|
||||
}
|
||||
|
||||
const fail = (what: string): never => {
|
||||
throw new Error(`${what} is missing`);
|
||||
};
|
||||
|
||||
/** A theme with one colour changed, for a case the built-in themes do not reach. */
|
||||
function themed(id: Parameters<typeof bookThemeOf>[0], palette: Partial<BookTheme['palette']>): BookTheme {
|
||||
const theme = bookThemeOf(id);
|
||||
return { ...theme, palette: { ...theme.palette, ...palette } };
|
||||
}
|
||||
|
||||
describe('pouring a page into a template', () => {
|
||||
it('TPLAPPLY-001: puts the page photos into the photo roles in order, keeps their focal points and parks the rest', () => {
|
||||
const spread = page(
|
||||
'cover',
|
||||
[1, 2, 3, 4, 5, 6].map((n) => photo(`ph${n}`, 10 + n, n === 1 ? { focalX: 0.2 } : {})),
|
||||
);
|
||||
const out = lay(spread, 'grid-badge-circle');
|
||||
expect(photosOf(out).map((p) => (p.kind === 'photo' ? p.photoId : null))).toEqual([11, 12, 13, 14]);
|
||||
expect(photosOf(out)[0]).toMatchObject({ focalX: 0.2 });
|
||||
expect(out.parked.map((p) => (p.kind === 'photo' ? p.photoId : null))).toEqual([15, 16]);
|
||||
const few = lay(page('cover', [photo('ph1', 11)]), 'grid-badge-circle');
|
||||
expect(photosOf(few).map((p) => (p.kind === 'photo' ? p.photoId : null))).toEqual([11, null, null, null]);
|
||||
});
|
||||
|
||||
it('TPLAPPLY-002: sets the page words in the roles they read as, keeping their binding, and parks what has no role', () => {
|
||||
const story = 'We walked the length of the coast road and back again, stopping at every bay. '.repeat(3);
|
||||
const spread = page('cover', [
|
||||
text('t1', 'Portugal', { size: 30, binding: { source: 'journey.title' } }),
|
||||
text('t2', '2026'),
|
||||
text('t3', story),
|
||||
]);
|
||||
const out = lay(spread, 'postcard-houses');
|
||||
expect(textOf(out, 'Portugal')?.binding).toEqual({ source: 'journey.title' });
|
||||
expect(textOf(out, '2026')).toBeDefined();
|
||||
expect(out.parked.map((e) => (e.kind === 'text' ? e.text : e.kind))).toEqual([story]);
|
||||
// The words took the template's look, not their own.
|
||||
expect(textOf(out, 'Portugal')?.font).toBe('dmserif');
|
||||
});
|
||||
|
||||
it('TPLAPPLY-003: fills empty roles with the words given and leaves out a role that has none', () => {
|
||||
const out = lay(page('cover', []), 'minimal-serif-classic', square, {
|
||||
words: { title: { text: 'Iceland' }, date: { text: '2025' } },
|
||||
});
|
||||
const texts = out.elements.filter((e): e is BookTextElement => e.kind === 'text').map((e) => e.text);
|
||||
expect(texts).toEqual(['Iceland', '2025']);
|
||||
expect(out.elements.some((e) => e.kind === 'text' && !e.text)).toBe(false);
|
||||
});
|
||||
|
||||
it('TPLAPPLY-004: takes a page made from an entry its words, bound to the entry, and its date as a mark', () => {
|
||||
const spread = page('inner', [photo('ph1', 21)], { entryId: 7 });
|
||||
const words = bookTemplateWords(spread, {
|
||||
bookTitle: 'Portugal',
|
||||
locale: 'en',
|
||||
entries: [
|
||||
{ id: 7, title: 'Sintra', location: 'Serra de Sintra', date: '2026-05-14', story: 'Up through the fog.' },
|
||||
],
|
||||
});
|
||||
const out = lay(spread, 'day-opener', square, { words });
|
||||
expect(textOf(out, 'Sintra')?.binding).toEqual({ source: 'entry.title', entryId: 7 });
|
||||
expect(textOf(out, 'Up through the fog.')?.binding).toEqual({ source: 'entry.story', entryId: 7 });
|
||||
expect(out.elements.find((e) => e.kind === 'badge')).toMatchObject({ variant: 'date', text: '14', sub: 'MAY' });
|
||||
});
|
||||
|
||||
it('TPLAPPLY-005: fills the letters of a word-art title with the photo its role names, and colours them without one', () => {
|
||||
const withPhoto = lay(page('inner', [photo('ph1', 31, { focalX: 0.3 })]), 'hero-trio-wordart', square, {
|
||||
words: { title: { text: 'Sintra' } },
|
||||
});
|
||||
expect(textOf(withPhoto, 'Sintra')?.fillPaint).toEqual({
|
||||
type: 'photo',
|
||||
photoId: 31,
|
||||
focalX: 0.3,
|
||||
focalY: 0.5,
|
||||
zoom: 1,
|
||||
});
|
||||
const without = lay(page('inner', []), 'hero-trio-wordart', square, { words: { title: { text: 'Sintra' } } });
|
||||
expect(textOf(without, 'Sintra')?.fillPaint).toBeNull();
|
||||
});
|
||||
|
||||
it('TPLAPPLY-006: keeps locked elements where they were and drops the old design', () => {
|
||||
const lockedShape = bookShapeElementSchema.parse({
|
||||
id: 'keep-me',
|
||||
kind: 'shape',
|
||||
frame: { x: 1, y: 1, w: 5, h: 5 },
|
||||
locked: true,
|
||||
});
|
||||
const old = bookShapeElementSchema.parse({ id: 'old', kind: 'shape', frame: { x: 9, y: 9, w: 5, h: 5 } });
|
||||
const out = lay(page('cover', [lockedShape, old, photo('ph1', 41)]), 'minimal-serif-window');
|
||||
expect(out.elements[0]).toEqual(lockedShape);
|
||||
expect(out.elements.some((e) => e.id === 'old')).toBe(false);
|
||||
});
|
||||
|
||||
it('TPLAPPLY-007: pours the page map into the map role in the template look, or a photo when there is no map', () => {
|
||||
const own = bookMapElementSchema.parse({
|
||||
id: 'map',
|
||||
kind: 'map',
|
||||
frame: { x: 0, y: 0, w: 50, h: 50 },
|
||||
countries: ['PT'],
|
||||
points: [{ lat: 38.7, lng: -9.1, label: 'Lisbon' }],
|
||||
style: 'dark',
|
||||
});
|
||||
const out = lay(page('inner', [own, photo('ph1', 51)]), 'map-tickets');
|
||||
const map = out.elements.find((e) => e.kind === 'map');
|
||||
expect(map).toMatchObject({ countries: ['PT'], style: 'minimal' });
|
||||
expect(map?.kind === 'map' && map.points[0]?.label).toBe('Lisbon');
|
||||
const mapless = lay(
|
||||
page(
|
||||
'inner',
|
||||
[1, 2, 3].map((n) => photo(`ph${n}`, 60 + n)),
|
||||
),
|
||||
'map-tickets',
|
||||
);
|
||||
expect(mapless.elements.some((e) => e.kind === 'map')).toBe(false);
|
||||
expect(photosOf(mapless).map((p) => (p.kind === 'photo' ? p.photoId : null))).toEqual([63, 61, 62]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the theme of the book', () => {
|
||||
it('TPLAPPLY-008: sets the linked colours and faces in the theme, and tints what the document links nowhere', () => {
|
||||
const nordic = bookThemeOf('nordic');
|
||||
const out = lay(page('cover', [photo('ph1', 71)]), 'postcard-houses', square, {
|
||||
theme: nordic,
|
||||
words: { title: { text: 'Bergen' } },
|
||||
});
|
||||
expect(textOf(out, 'Bergen')).toMatchObject({ color: nordic.palette.ink, font: 'bricolage', weight: 700 });
|
||||
expect(out.background).toBe(nordic.palette.paper);
|
||||
const houses = out.elements.find((e) => e.kind === 'sticker' && e.stickerId === 'rowHouses');
|
||||
const { accent, accent2, accent3, ink } = nordic.palette;
|
||||
expect(houses?.kind === 'sticker' && houses.colors).toEqual([accent3, accent, ink, accent2]);
|
||||
});
|
||||
|
||||
it('TPLAPPLY-009: keeps the drawn colours and the links without a theme, so a theme set later recolours the page', () => {
|
||||
const out = lay(page('cover', []), 'postcard-houses', square, { words: { title: { text: 'Bergen' } } });
|
||||
expect(textOf(out, 'Bergen')).toMatchObject({ color: '#1f2d4d', token: 'ink', fontRole: 'display' });
|
||||
expect(out.backgroundToken).toBe('paper');
|
||||
});
|
||||
|
||||
it('TPLAPPLY-010: takes a pattern and a shadow from the theme, and sets words on a colour in ink or paper so they read', () => {
|
||||
const nordic = bookThemeOf('nordic');
|
||||
const row = lay(page('cover', [photo('ph1', 81)]), 'stamp-collage-row', square, { theme: nordic });
|
||||
const band = row.elements.find((e) => e.kind === 'shape' && e.paint?.type === 'pattern');
|
||||
expect(band?.kind === 'shape' && band.paint).toMatchObject({
|
||||
fg: nordic.palette.accent3,
|
||||
bg: nordic.palette.paper,
|
||||
});
|
||||
const stamp = photosOf(row)[0];
|
||||
expect(stamp?.kind === 'photo' && stamp.shadow?.color).toBe(nordic.palette.ink);
|
||||
|
||||
const pale = themed('minimal-serif', { accent: '#f4f4f4' });
|
||||
const band2 = lay(page('cover', []), 'spine-band-stitched', square, {
|
||||
theme: pale,
|
||||
words: { title: { text: 'Oslo' } },
|
||||
});
|
||||
expect(textOf(band2, 'Oslo')).toMatchObject({ token: 'ink', color: pale.palette.ink });
|
||||
});
|
||||
});
|
||||
|
||||
describe('scaling to the book', () => {
|
||||
it('TPLAPPLY-011: lays the fractions on an A4 landscape page, bleeds on its bleed and keeps a circle round', () => {
|
||||
const out = lay(page('cover', []), synthetic(), landscape, { words: { title: { text: 'Ice' } } });
|
||||
const [picture, circle] = out.elements;
|
||||
expect(picture?.frame).toEqual({ x: -3, y: -3, w: 151.5, h: 216 });
|
||||
expect(circle?.frame.w).toBe(circle?.frame.h);
|
||||
expect(circle?.frame).toEqual({ x: 186.9, y: 21, w: 42, h: 42 });
|
||||
expect(out.elements.find((e) => e.kind === 'line')).toMatchObject({ strokeWidth: 0.6 });
|
||||
});
|
||||
|
||||
it('TPLAPPLY-012: scales every length with the shorter side of a larger page', () => {
|
||||
const out = lay(page('cover', []), synthetic(), big, { words: { title: { text: 'Ice' } } });
|
||||
const title = textOf(out, 'Ice');
|
||||
expect(title?.size).toBeCloseTo(24 * (300 / 210), 1);
|
||||
expect(photosOf(out)[0]?.frame).toEqual({ x: -3, y: -3, w: 153, h: 306 });
|
||||
expect(out.elements.find((e) => e.kind === 'shape')?.frame).toEqual({ x: 180, y: 30, w: 60, h: 60 });
|
||||
});
|
||||
|
||||
it('TPLAPPLY-013: gives the template groups ids of the page and tints a sticker and a border from the theme', () => {
|
||||
const theme = bookThemeOf('riviera');
|
||||
const out = lay(page('cover', [photo('ph1', 91)]), synthetic(), square, {
|
||||
theme,
|
||||
words: { title: { text: 'Nice' } },
|
||||
});
|
||||
const grouped = out.elements.filter((e) => e.group);
|
||||
expect(grouped.map((e) => e.kind)).toEqual(['shape', 'text']);
|
||||
expect(new Set(grouped.map((e) => e.group)).size).toBe(1);
|
||||
expect(grouped[0]?.group).not.toBe('g1');
|
||||
const lemon = out.elements.find((e) => e.kind === 'sticker');
|
||||
expect(lemon?.kind === 'sticker' && lemon.colors).toEqual([theme.palette.accent, '#222222']);
|
||||
expect(photosOf(out)[0]).toMatchObject({ border: { width: 3, color: theme.palette.accent } });
|
||||
expect(out.elements.find((e) => e.kind === 'line')).toMatchObject({ stroke: theme.palette.accent2 });
|
||||
});
|
||||
|
||||
it('TPLAPPLY-014: fits a long title below the size it was drawn at, which stays its ceiling, and keeps small type readable', () => {
|
||||
const out = lay(page('cover', []), 'cutout-matisse', square, {
|
||||
words: { title: { text: 'Portugal and the whole of the Algarve' } },
|
||||
});
|
||||
const title = textOf(out, 'Portugal and the whole of the Algarve');
|
||||
expect(title?.size).toBeLessThan(140);
|
||||
expect(title?.fitMax).toBeCloseTo(140, 1);
|
||||
// Shortened later, the title grows back to the size the template drew it at.
|
||||
const shorter = {
|
||||
...out,
|
||||
elements: out.elements.map((e) => (e.id === title?.id && e.kind === 'text' ? { ...e, text: 'Oslo' } : e)),
|
||||
};
|
||||
expect(textOf(fitSpreadTexts(shorter, out), 'Oslo')?.size).toBeCloseTo(140, 1);
|
||||
const small = lay(page('inner', []), 'filmstrip-script', bookPageSetupForPreset('a5-landscape'), {
|
||||
words: { caption: { text: 'The ferry out' } },
|
||||
});
|
||||
expect(textOf(small, 'The ferry out')?.size).toBeGreaterThanOrEqual(6);
|
||||
});
|
||||
|
||||
it('TPLAPPLY-015: keeps a picture across the fold off it, unless its own focal point already is', () => {
|
||||
const centred = lay(page('inner', [photo('ph1', 95)]), 'bleed-band-caption');
|
||||
expect(photosOf(centred)[0]).toMatchObject({ focalX: 0.7 });
|
||||
const aside = lay(page('inner', [photo('ph1', 95, { focalX: 0.9 })]), 'bleed-band-caption');
|
||||
expect(photosOf(aside)[0]).toMatchObject({ focalX: 0.9 });
|
||||
});
|
||||
|
||||
it('TPLAPPLY-017: keeps a lockup of the template as one group of the page, and a badge with its words on any shape of page', () => {
|
||||
const out = lay(page('cover', []), 'minimal-serif-classic', square, {
|
||||
words: { title: { text: 'Iceland' }, subtitle: { text: 'Ring road' }, date: { text: '2025' } },
|
||||
});
|
||||
const grouped = out.elements.filter((e) => e.group);
|
||||
expect(grouped.map((e) => (e.kind === 'text' ? e.text : e.kind))).toEqual(['Iceland', 'Ring road', 'line', '2025']);
|
||||
expect(new Set(grouped.map((e) => e.group)).size).toBe(1);
|
||||
const alone = lay(page('cover', []), 'minimal-serif-classic', square, { words: { title: { text: 'Iceland' } } });
|
||||
expect(alone.elements.filter((e) => e.group).map((e) => e.kind)).toEqual(['text', 'line']);
|
||||
|
||||
const wide = lay(page('cover', []), 'grid-badge-circle', landscape, { words: { title: { text: 'Iceland' } } });
|
||||
const ring = wide.elements.find((e) => e.kind === 'shape' && e.shape === 'ellipse');
|
||||
const words = wide.elements.find((e) => e.kind === 'text');
|
||||
expect(ring?.group).toBeDefined();
|
||||
expect(words?.group).toBe(ring?.group);
|
||||
if (!ring || !words) throw new Error('the badge is missing');
|
||||
expect(words.frame.x).toBeGreaterThanOrEqual(ring.frame.x);
|
||||
expect(words.frame.x + words.frame.w).toBeLessThanOrEqual(ring.frame.x + ring.frame.w);
|
||||
});
|
||||
|
||||
it('TPLAPPLY-018: sets the page date line in a date mark only when the mark is made of it, and parks it otherwise', () => {
|
||||
const spread = (els: BookElement[]) =>
|
||||
page('inner', [text('t0', 'Sintra', { size: 30, binding: { source: 'entry.title', entryId: 7 } }), ...els], {
|
||||
entryId: 7,
|
||||
});
|
||||
const words = {
|
||||
date: { text: '14 May 2026', binding: { source: 'entry.date' as const, entryId: 7, value: '2026-05-14' } },
|
||||
};
|
||||
const plain = lay(spread([text('t1', 'Summer 2025')]), 'day-opener', square, { words });
|
||||
expect(plain.elements.find((e) => e.kind === 'badge')).toMatchObject({ text: '14', sub: 'MAY' });
|
||||
expect(plain.parked.map((e) => (e.kind === 'text' ? e.text : e.kind))).toEqual(['Summer 2025']);
|
||||
|
||||
const bound = text('t2', '3 June 2026', { binding: { source: 'entry.date', entryId: 7, value: '2026-06-03' } });
|
||||
const marked = lay(spread([bound]), 'day-opener', square, { words });
|
||||
expect(marked.elements.find((e) => e.kind === 'badge')).toMatchObject({ text: '3', sub: 'JUNE' });
|
||||
expect(marked.parked).toEqual([]);
|
||||
|
||||
const own = bookBadgeElementSchema.parse({
|
||||
id: 'b1',
|
||||
kind: 'badge',
|
||||
variant: 'date',
|
||||
frame: { x: 0, y: 0, w: 20, h: 20 },
|
||||
text: '9',
|
||||
sub: 'JULY',
|
||||
});
|
||||
const kept = lay(spread([own, bound]), 'day-opener', square, { words });
|
||||
expect(kept.elements.find((e) => e.kind === 'badge')).toMatchObject({ text: '9', sub: 'JULY' });
|
||||
expect(kept.parked.map((e) => (e.kind === 'text' ? e.text : e.kind))).toEqual(['3 June 2026']);
|
||||
});
|
||||
|
||||
it('TPLAPPLY-019: takes a quotation mark set as text for design, never for the words of the next layout', () => {
|
||||
const words = { title: { text: 'Sintra' }, body: { text: 'Up through the fog to the palace.' } };
|
||||
const quoted = lay(page('inner', [photo('ph1', 5)]), 'story-quote', square, { words });
|
||||
expect(textOf(quoted, '\u201c')).toBeDefined();
|
||||
const next = lay(quoted, 'hero-script-word', square, { words: {} });
|
||||
expect(next.elements.some((e) => e.kind === 'text' && e.text === '\u201c')).toBe(false);
|
||||
expect(next.parked.some((e) => e.kind === 'text' && e.text === '\u201c')).toBe(false);
|
||||
expect(textOf(next, 'Sintra')).toBeDefined();
|
||||
|
||||
const builtIn = applyBookTemplate(quoted, findBookTemplate('hero-story') ?? fail('hero-story'), square);
|
||||
const heading = builtIn.elements.find((e): e is BookTextElement => e.kind === 'text' && e.size === 22);
|
||||
expect(heading?.text).toBe('Sintra');
|
||||
expect(builtIn.elements.some((e) => e.kind === 'text' && e.text === '\u201c')).toBe(false);
|
||||
});
|
||||
|
||||
it('TPLAPPLY-020: sets the words on a colour so they read in the theme at the size they were fitted to', () => {
|
||||
const small = bookPageSetupForPreset('a5-landscape');
|
||||
const all = {
|
||||
title: { text: 'Portugal' },
|
||||
subtitle: { text: 'Lisbon to the Algarve' },
|
||||
date: { text: '2026' },
|
||||
};
|
||||
for (const [id, theme] of [
|
||||
['grid-badge-seal', 'minimal-serif'],
|
||||
['grid-badge-seal', 'scrapbook-script'],
|
||||
['poster-pop-sunburst', 'riviera'],
|
||||
['poster-pop-stack', 'nordic'],
|
||||
] as const) {
|
||||
const out = lay(page('cover', [photo('ph1', 3)]), id, small, { theme: bookThemeOf(theme), words: all });
|
||||
const low = designFindings(out, small).filter((f) => f.code === 'contrast_low');
|
||||
expect(low, `${id} in ${theme}`).toEqual([]);
|
||||
}
|
||||
// Where neither ink nor paper reads on the colour, the words go on a band of the other.
|
||||
const sunflower = themed('poster-pop', { accent2: '#7a7a7a' });
|
||||
const out = lay(page('cover', [photo('ph1', 3)]), 'poster-pop-sunburst', small, { theme: sunflower, words: all });
|
||||
const subtitle = textOf(out, 'Lisbon to the Algarve');
|
||||
expect(subtitle?.highlight).toBeTruthy();
|
||||
if (!subtitle?.highlight) throw new Error('no band');
|
||||
expect(contrastRatio(subtitle.color, subtitle.highlight.color)).toBeGreaterThanOrEqual(
|
||||
requiredContrast(subtitle.size),
|
||||
);
|
||||
});
|
||||
|
||||
it('TPLAPPLY-021: keeps the name of a locked group when the page is laid out again', () => {
|
||||
const lockedShape = (id: string) =>
|
||||
bookShapeElementSchema.parse({
|
||||
id,
|
||||
kind: 'shape',
|
||||
frame: { x: 1, y: 1, w: 5, h: 5 },
|
||||
locked: true,
|
||||
group: 'g-keep',
|
||||
});
|
||||
const spread = page('cover', [lockedShape('a'), lockedShape('b'), photo('ph1', 4)], {
|
||||
groups: [{ id: 'g-keep', name: 'My lockup' }],
|
||||
});
|
||||
const out = lay(spread, 'minimal-serif-classic', square, { words: { title: { text: 'Oslo' } } });
|
||||
expect(out.groups).toContainEqual({ id: 'g-keep', name: 'My lockup' });
|
||||
});
|
||||
|
||||
it('TPLAPPLY-016: makes a valid page of every template, on every page format, with and without a theme', () => {
|
||||
const words = {
|
||||
title: { text: 'Portugal' },
|
||||
subtitle: { text: 'Lisbon to the Algarve' },
|
||||
date: { text: '2026', binding: { source: 'entry.date' as const, entryId: 7, value: '2026-05-14' } },
|
||||
body: { text: 'We took the early train out of Lisbon and walked up through the fog to the palace.' },
|
||||
caption: { text: 'Pena Palace in the fog' },
|
||||
};
|
||||
const formats = ['square-210', 'a4-landscape', 'a4-portrait', 'a5-landscape'] as const;
|
||||
for (const preset of formats) {
|
||||
for (const theme of [null, bookThemeOf('cutout')]) {
|
||||
for (const t of BOOK_DECORATED_TEMPLATES) {
|
||||
const spread = page(
|
||||
t.kind === 'single' ? 'cover' : 'inner',
|
||||
[1, 2, 3, 4, 5, 6, 7, 8].map((n) => photo(`p${n}`, n)),
|
||||
);
|
||||
const out = lay(spread, t, bookPageSetupForPreset(preset), { theme, words });
|
||||
const parsed = bookSpreadSchema.safeParse(out);
|
||||
expect(parsed.success, `${t.id} on ${preset}`).toBe(true);
|
||||
expect(out.elements.length, t.id).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,276 @@
|
||||
import { gatherGroups, regroupCopies, tidyGroups } from '../book-groups';
|
||||
import { bookElementId } from '../book-ids';
|
||||
import { isBookOrnamentText } from '../book-layouts';
|
||||
import { bookDateMarkValue } from '../book-text';
|
||||
import { fitSpreadTexts } from '../book-text-fit';
|
||||
import { bookTravelMap, type BookTravelContext } from '../book-travel';
|
||||
import {
|
||||
MAX_SPREAD_ELEMENTS,
|
||||
type BookBadgeElement,
|
||||
type BookElement,
|
||||
type BookMapElement,
|
||||
type BookPageSetup,
|
||||
type BookPhotoElement,
|
||||
type BookSpread,
|
||||
type BookTextElement,
|
||||
} from '../book.schema';
|
||||
import { themedSpread } from '../theme/book-theme-apply';
|
||||
import type { BookTheme } from '../theme/book-theme.schema';
|
||||
import { templatePhotoIndex, type BookDecoratedTemplate, type BookTemplateTextRole } from './template-format';
|
||||
import {
|
||||
layTemplateElement,
|
||||
readableOn,
|
||||
templateBackgroundPaint,
|
||||
templatePage,
|
||||
type LaidElement,
|
||||
} from './template-lay';
|
||||
import { assignTemplateTexts, textRolesOf, type BookTemplateWord, type BookTemplateWords } from './template-words';
|
||||
|
||||
/**
|
||||
* Laying a page out on a decorated template.
|
||||
*
|
||||
* The same bargain as every layout in the Studio: the page's pictures go into
|
||||
* the template's photo frames in order (`photo:1` first), its words into the
|
||||
* template's text roles (template-words.ts says which into which), its date
|
||||
* into a date mark, its map into the template's map, and what the template
|
||||
* has no room for is parked with the page rather than lost. Everything else on
|
||||
* the page was the old design and goes, as with the built-in layouts; locked
|
||||
* elements stay where they are, in their place in the paint order.
|
||||
*
|
||||
* What the template brings is its design: panels, shapes, stickers, lines,
|
||||
* every look and every type setting. Its colours and faces follow the book's
|
||||
* theme where they are linked (template-format.ts), and its fitted titles
|
||||
* are fitted to the words they were given.
|
||||
*/
|
||||
|
||||
export interface ApplyDecoratedTemplateOptions {
|
||||
/** The book's theme; without one the template keeps the colours and faces it was drawn in. */
|
||||
theme?: BookTheme | null;
|
||||
/** Words for the roles the page has no words for (bookTemplateWords). */
|
||||
words?: BookTemplateWords;
|
||||
/** The journey's figures and way, for a map role on a page that has no map. */
|
||||
travel?: BookTravelContext | null;
|
||||
/** The book's language, for the month a date mark spells out. */
|
||||
locale?: string;
|
||||
newId?: (prefix: string) => string;
|
||||
}
|
||||
|
||||
type TextEl = BookTextElement;
|
||||
|
||||
/** The map's journey in the template's look: its data from the page's own map or from the journey. */
|
||||
function pouredMap(
|
||||
slot: BookMapElement,
|
||||
own: BookMapElement | undefined,
|
||||
travel: BookTravelContext | null | undefined,
|
||||
): BookMapElement | null {
|
||||
const from =
|
||||
own ??
|
||||
(travel?.stats ? bookTravelMap(travel, travel.stats, { id: slot.id, frame: slot.frame, scope: 'journey' }) : null);
|
||||
if (!from) return null;
|
||||
const { countries, points, path, roads, tripId, source, tileUrl, attribution, zoom } = from;
|
||||
return { ...slot, countries, points, path, roads, tripId, source, tileUrl, attribution, zoom };
|
||||
}
|
||||
|
||||
/** A role's text in the template's look, set in the page's words and keeping their link to the journal. */
|
||||
function pouredText(slot: TextEl, src: TextEl | undefined, word: BookTemplateWord | undefined): TextEl | null {
|
||||
if (src) return { ...slot, text: src.text, binding: src.binding, overridden: src.overridden };
|
||||
if (!word?.text.trim()) return null;
|
||||
return { ...slot, text: word.text, binding: word.binding ?? null, overridden: false };
|
||||
}
|
||||
|
||||
/** A map with nothing to draw laid as a photo frame in its place, with the next of the page's pictures. */
|
||||
function mapAsPhoto(slot: BookMapElement, src: BookPhotoElement | undefined): BookPhotoElement {
|
||||
return {
|
||||
id: slot.id,
|
||||
kind: 'photo',
|
||||
frame: slot.frame,
|
||||
rotation: slot.rotation,
|
||||
opacity: slot.opacity,
|
||||
locked: false,
|
||||
photoId: src?.photoId ?? null,
|
||||
fit: 'cover',
|
||||
focalX: src?.focalX ?? 0.5,
|
||||
focalY: src?.focalY ?? 0.5,
|
||||
radius: 0,
|
||||
filter: 'none',
|
||||
mask: null,
|
||||
frameStyle: 'none',
|
||||
};
|
||||
}
|
||||
|
||||
/** Closer than this to the fold, in millimetres, a picture's focal point is lost in the binding. */
|
||||
const FOLD_REACH = 12;
|
||||
|
||||
/**
|
||||
* A photo frame of the template with the page's picture in it; the frame
|
||||
* keeps the template's look. A frame drawn across the fold of a spread keeps
|
||||
* the picture's focal point unless that would put it in the fold: then it
|
||||
* takes the one the template was drawn with, which keeps it on one page.
|
||||
*/
|
||||
function pouredPhoto(slot: BookPhotoElement, src: BookPhotoElement | undefined, fold: number | null): BookPhotoElement {
|
||||
if (!src) return { ...slot, photoId: null };
|
||||
const inFold = fold !== null && Math.abs(slot.frame.x + src.focalX * slot.frame.w - fold) < FOLD_REACH;
|
||||
return { ...slot, photoId: src.photoId, focalX: inFold ? slot.focalX : src.focalX, focalY: src.focalY };
|
||||
}
|
||||
|
||||
/**
|
||||
* A date mark of the template with the page's date: the values of the page's
|
||||
* own mark, else the date the page's date line is bound to, else the date of
|
||||
* the words given for the role. `fromText` says the mark was made of the date
|
||||
* line, which then goes into it; any other date line stays with the page.
|
||||
*/
|
||||
function pouredMark(
|
||||
slot: BookBadgeElement,
|
||||
own: BookBadgeElement | undefined,
|
||||
src: TextEl | undefined,
|
||||
word: BookTemplateWord | undefined,
|
||||
locale: string,
|
||||
): { mark: BookBadgeElement | null; fromText: boolean } {
|
||||
if (own) return { mark: { ...slot, text: own.text, sub: own.sub, code: own.code }, fromText: false };
|
||||
if (slot.variant !== 'date') return { mark: null, fromText: false };
|
||||
const ofText = bookDateMarkValue(src?.binding?.value, locale);
|
||||
if (ofText) return { mark: { ...slot, ...ofText }, fromText: true };
|
||||
const ofWord = bookDateMarkValue(word?.binding?.value, locale);
|
||||
return { mark: ofWord ? { ...slot, ...ofWord } : null, fromText: false };
|
||||
}
|
||||
|
||||
interface Pool {
|
||||
photos: BookPhotoElement[];
|
||||
texts: TextEl[];
|
||||
map: BookMapElement | undefined;
|
||||
marks: BookBadgeElement[];
|
||||
}
|
||||
|
||||
/** What a page brings to a template: its pictures, its words (a mark set as text is design, not words), its map and its marks. */
|
||||
function poolOf(spread: BookSpread): Pool {
|
||||
const all = [...spread.elements.filter((e) => !e.locked), ...spread.parked];
|
||||
return {
|
||||
photos: all.filter((e): e is BookPhotoElement => e.kind === 'photo' && e.photoId != null),
|
||||
texts: all.filter((e): e is TextEl => e.kind === 'text' && !!e.text.trim() && !isBookOrnamentText(e)),
|
||||
map: all.find((e): e is BookMapElement => e.kind === 'map'),
|
||||
marks: all.filter((e): e is BookBadgeElement => e.kind === 'badge'),
|
||||
};
|
||||
}
|
||||
|
||||
/** The letters of a text filled with the picture a photo role took, or left in their colour without one. */
|
||||
function withFill(el: TextEl, photo: BookPhotoElement | undefined): TextEl {
|
||||
if (photo?.photoId == null) return { ...el, fillPaint: null };
|
||||
return {
|
||||
...el,
|
||||
fillPaint: { type: 'photo', photoId: photo.photoId, focalX: photo.focalX, focalY: photo.focalY, zoom: 1 },
|
||||
};
|
||||
}
|
||||
|
||||
const isTextRole = (role: string | undefined, roles: ReadonlySet<BookTemplateTextRole>): role is BookTemplateTextRole =>
|
||||
role !== undefined && roles.has(role as BookTemplateTextRole);
|
||||
|
||||
/** The template's elements laid on the page, the page's content poured into their roles; `fold` is a spread's. */
|
||||
function pour(laid: readonly LaidElement[], pool: Pool, fold: number | null, opts: ApplyDecoratedTemplateOptions) {
|
||||
const photoOrder = laid
|
||||
.map((l) => templatePhotoIndex(l.role))
|
||||
.filter((n): n is number => n !== null)
|
||||
.sort((a, b) => a - b);
|
||||
const photoOf = new Map(photoOrder.map((n, i) => [n, pool.photos[i]]));
|
||||
// A map role with nothing to draw takes the next picture instead, so its place is never an empty hole.
|
||||
const mapless = !pool.map && !opts.travel?.stats && laid.some((l) => l.role === 'map');
|
||||
const spare = mapless ? pool.photos[photoOrder.length] : undefined;
|
||||
const roles = textRolesOf(laid.flatMap((l) => (l.role ? [l.role] : [])));
|
||||
const textOf = assignTemplateTexts(pool.texts, roles);
|
||||
const usedTexts = new Set(textOf.values());
|
||||
const locale = opts.locale ?? 'en';
|
||||
|
||||
const out: BookElement[] = [];
|
||||
for (const { el, role, fillPhoto } of laid) {
|
||||
const photo = templatePhotoIndex(role);
|
||||
if (el.kind === 'photo' && photo !== null) out.push(pouredPhoto(el, photoOf.get(photo), fold));
|
||||
else if (el.kind === 'text' && isTextRole(role, roles)) {
|
||||
const text = pouredText(el, textOf.get(role), opts.words?.[role]);
|
||||
const fill = templatePhotoIndex(fillPhoto);
|
||||
if (text) out.push(fill === null ? text : withFill(text, photoOf.get(fill)));
|
||||
} else if (el.kind === 'badge' && isTextRole(role, roles)) {
|
||||
const src = textOf.get(role);
|
||||
const own = pool.marks.find((m) => m.variant === el.variant);
|
||||
const { mark, fromText } = pouredMark(el, own, src, opts.words?.[role], locale);
|
||||
if (mark) out.push(mark);
|
||||
// A date line the mark was not made of stays with the page rather than going with the mark.
|
||||
if (src && !fromText) usedTexts.delete(src);
|
||||
} else if (el.kind === 'map' && role === 'map') {
|
||||
out.push(pouredMap(el, pool.map, opts.travel) ?? mapAsPhoto(el, spare));
|
||||
} else if (!role) out.push(el);
|
||||
}
|
||||
const placed = photoOrder.length + (mapless ? 1 : 0);
|
||||
const parked = [...pool.photos.slice(placed), ...pool.texts.filter((t) => !usedTexts.has(t))].slice(
|
||||
0,
|
||||
MAX_SPREAD_ELEMENTS,
|
||||
);
|
||||
return { elements: out, parked };
|
||||
}
|
||||
|
||||
/** Locked elements back where they were in the paint order, over the laid page. */
|
||||
function withLocked(spread: BookSpread, elements: BookElement[]): BookElement[] {
|
||||
const next = elements.slice(0, MAX_SPREAD_ELEMENTS);
|
||||
spread.elements.forEach((el, i) => {
|
||||
if (el.locked) next.splice(Math.min(i, next.length), 0, el);
|
||||
});
|
||||
return next.slice(0, MAX_SPREAD_ELEMENTS);
|
||||
}
|
||||
|
||||
/** Every element of a template laid on a page of this book, nothing poured into it yet. */
|
||||
export function layDecoratedTemplate(
|
||||
tpl: BookDecoratedTemplate,
|
||||
page: BookPageSetup,
|
||||
theme: BookTheme | null,
|
||||
newId: (prefix: string) => string,
|
||||
): LaidElement[] {
|
||||
const at = templatePage(page, tpl.kind === 'inner' ? 2 : 1);
|
||||
return tpl.elements.flatMap((src) => layTemplateElement(src, at, theme, newId(src.kind.charAt(0))) ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* The spread laid out on a decorated template. The caller checks that the
|
||||
* template fits the page (a single page takes a `single` template, a double
|
||||
* spread an `inner` one); laid on the other kind it still makes a valid page,
|
||||
* only not a good one.
|
||||
*/
|
||||
export function applyDecoratedTemplate(
|
||||
spread: BookSpread,
|
||||
tpl: BookDecoratedTemplate,
|
||||
page: BookPageSetup,
|
||||
opts: ApplyDecoratedTemplateOptions = {},
|
||||
): BookSpread {
|
||||
const newId = opts.newId ?? bookElementId;
|
||||
const theme = opts.theme ?? null;
|
||||
const laid = layDecoratedTemplate(tpl, page, theme, newId);
|
||||
const fold = tpl.kind === 'inner' ? page.pageWidth : null;
|
||||
const { elements, parked } = pour(laid, poolOf(spread), fold, opts);
|
||||
// The template's groups get ids of this page; a group left with one member is no group (tidyGroups).
|
||||
const regrouped = regroupCopies(elements, undefined, newId);
|
||||
|
||||
const { backgroundPaint: _old, backgroundToken: _link, groups: _groups, ...rest } = spread;
|
||||
const paint = templateBackgroundPaint(tpl, theme);
|
||||
const lockedGroups = (spread.groups ?? []).filter((g) => spread.elements.some((e) => e.locked && e.group === g.id));
|
||||
let next: BookSpread = {
|
||||
...rest,
|
||||
background: tpl.background,
|
||||
...(tpl.backgroundToken ? { backgroundToken: tpl.backgroundToken } : {}),
|
||||
...(paint ? { backgroundPaint: paint } : {}),
|
||||
...(lockedGroups.length ? { groups: lockedGroups } : {}),
|
||||
elements: withLocked(spread, regrouped.elements),
|
||||
parked,
|
||||
};
|
||||
if (theme) next = themedSpread(next, theme);
|
||||
// Fitted to the words they were given, below the size the template drew them at, which stays their ceiling.
|
||||
next = fitSpreadTexts(tidyGroups(gatherGroups(next)));
|
||||
return theme ? readableSpread(next, laid, theme) : next;
|
||||
}
|
||||
|
||||
/** Every laid text set on a colour made to read there, now that its words are in and its size is fitted. */
|
||||
function readableSpread(spread: BookSpread, laid: readonly LaidElement[], theme: BookTheme): BookSpread {
|
||||
const onOf = new Map(laid.flatMap((l) => (l.on ? [[l.el.id, l.on] as const] : [])));
|
||||
if (!onOf.size) return spread;
|
||||
const elements = spread.elements.map((e) => {
|
||||
const on = e.kind === 'text' ? onOf.get(e.id) : undefined;
|
||||
return on && e.kind === 'text' ? readableOn(e, on, theme) : e;
|
||||
});
|
||||
return elements.every((e, i) => e === spread.elements[i]) ? spread : { ...spread, elements };
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import { BOOK_COVER_ARCHETYPES } from '../book-archetypes';
|
||||
import { BOOK_COVER_TEMPLATES, BOOK_TEMPLATES } from '../book-layouts';
|
||||
import { bookPageSetupForPreset } from '../book-pages';
|
||||
import { MAX_SPREAD_ELEMENTS, type BookElement } from '../book.schema';
|
||||
import { listBookLayouts } from '../describe-spread';
|
||||
import { bookSticker } from '../stickers/sticker-catalogue';
|
||||
import { BOOK_COLOR_TOKENS, BOOK_FONT_SLOTS } from '../theme/book-theme.schema';
|
||||
import {
|
||||
BOOK_DECORATED_TEMPLATES,
|
||||
decoratedTemplateRow,
|
||||
decoratedTemplatesFor,
|
||||
findDecoratedTemplate,
|
||||
isBookTemplateId,
|
||||
} from './template-catalogue';
|
||||
import {
|
||||
BOOK_TEMPLATE_TEXT_ROLES,
|
||||
bookDecoratedTemplateSchema,
|
||||
templatePhotoIndex,
|
||||
templatePhotoSlots,
|
||||
templateRoles,
|
||||
type BookDecoratedTemplate,
|
||||
} from './template-format';
|
||||
import { layTemplateElement, templatePage } from './template-lay';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
// TPLCAT-001 to TPLCAT-010
|
||||
|
||||
/**
|
||||
* The decorated templates as data: the set the plan asks for, every template
|
||||
* in the format's contract, every element a valid document element once laid
|
||||
* on a page, its roles and links well formed, and every frame either on the
|
||||
* page or bleeding on purpose.
|
||||
*/
|
||||
|
||||
const square = bookPageSetupForPreset('square-210');
|
||||
const covers = BOOK_DECORATED_TEMPLATES.filter((t) => t.kind === 'single');
|
||||
const spreads = BOOK_DECORATED_TEMPLATES.filter((t) => t.kind === 'inner');
|
||||
|
||||
/** A template's elements on the 210 mm square, nothing poured in. */
|
||||
function laid(tpl: BookDecoratedTemplate) {
|
||||
const at = templatePage(square, tpl.kind === 'inner' ? 2 : 1);
|
||||
return tpl.elements.map((src, i) => ({ src, laid: layTemplateElement(src, at, null, `e${i}`) }));
|
||||
}
|
||||
|
||||
/** The box an element covers on the page, turned with it. */
|
||||
function box(el: BookElement) {
|
||||
const { x, y, w, h } = el.frame;
|
||||
const rad = (el.rotation * Math.PI) / 180;
|
||||
const bw = w * Math.abs(Math.cos(rad)) + h * Math.abs(Math.sin(rad));
|
||||
const bh = w * Math.abs(Math.sin(rad)) + h * Math.abs(Math.cos(rad));
|
||||
return { x: x + w / 2 - bw / 2, y: y + h / 2 - bh / 2, w: bw, h: bh };
|
||||
}
|
||||
|
||||
const camel = (id: string) => id.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase());
|
||||
|
||||
describe('the decorated template set', () => {
|
||||
it('TPLCAT-001: holds 21 covers, three in each archetype, and 16 double spreads', () => {
|
||||
expect(covers).toHaveLength(21);
|
||||
expect(spreads).toHaveLength(16);
|
||||
for (const archetype of BOOK_COVER_ARCHETYPES) {
|
||||
expect(decoratedTemplatesFor('single', archetype), archetype).toHaveLength(3);
|
||||
expect(decoratedTemplatesFor('inner', archetype).length, archetype).toBeGreaterThanOrEqual(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('TPLCAT-002: gives every template a unique id, distinct from the built-in layouts, and a label key from it', () => {
|
||||
const ids = BOOK_DECORATED_TEMPLATES.map((t) => t.id);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
const builtIn = new Set([...BOOK_TEMPLATES, ...BOOK_COVER_TEMPLATES].map((t) => t.id));
|
||||
for (const id of ids) expect(builtIn.has(id), id).toBe(false);
|
||||
for (const tpl of BOOK_DECORATED_TEMPLATES) expect(tpl.labelKey).toBe(`studio.templates.name.${camel(tpl.id)}`);
|
||||
});
|
||||
|
||||
it('TPLCAT-003: holds every template to the format, with tags and a line a model can choose by', () => {
|
||||
for (const tpl of BOOK_DECORATED_TEMPLATES) {
|
||||
const parsed = bookDecoratedTemplateSchema.safeParse(tpl);
|
||||
expect(parsed.success, `${tpl.id}: ${parsed.success ? '' : parsed.error.message}`).toBe(true);
|
||||
expect(tpl.descr, tpl.id).not.toMatch(/[\u2013\u2014]/);
|
||||
}
|
||||
});
|
||||
|
||||
it('TPLCAT-004: lays every element on a page as a valid document element, stickers from the library', () => {
|
||||
for (const tpl of BOOK_DECORATED_TEMPLATES) {
|
||||
expect(tpl.elements.length, tpl.id).toBeLessThanOrEqual(MAX_SPREAD_ELEMENTS);
|
||||
for (const { src, laid: out } of laid(tpl)) {
|
||||
expect(out, `${tpl.id} ${src.id}`).not.toBeNull();
|
||||
if (src.kind === 'sticker') expect(bookSticker(src.stickerId), `${tpl.id} ${src.stickerId}`).toBeDefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('TPLCAT-005: gives every role once, numbers the photo frames from 1 without a gap, and roles only what can take them', () => {
|
||||
for (const tpl of BOOK_DECORATED_TEMPLATES) {
|
||||
const roles = templateRoles(tpl);
|
||||
expect(new Set(roles).size, tpl.id).toBe(roles.length);
|
||||
const photos = roles.map(templatePhotoIndex).filter((n): n is number => n !== null);
|
||||
expect(
|
||||
[...photos].sort((a, b) => a - b),
|
||||
tpl.id,
|
||||
).toEqual(photos.map((_, i) => i + 1).sort((a, b) => a - b));
|
||||
expect(templatePhotoSlots(tpl), tpl.id).toBe(photos.length);
|
||||
for (const el of tpl.elements) {
|
||||
const where = `${tpl.id} ${el.id}`;
|
||||
if (el.kind === 'photo') expect(templatePhotoIndex(el.role), where).not.toBeNull();
|
||||
if (el.kind === 'map') expect(el.role, where).toBe('map');
|
||||
if (el.kind === 'badge') expect(el.role, where).toBe('date');
|
||||
if (el.role && (BOOK_TEMPLATE_TEXT_ROLES as readonly string[]).includes(el.role)) {
|
||||
expect(['text', 'badge'], where).toContain(el.kind);
|
||||
}
|
||||
if (el.fillPhoto) expect(roles, where).toContain(el.fillPhoto);
|
||||
}
|
||||
expect(roles, tpl.id).toContain('title');
|
||||
}
|
||||
});
|
||||
|
||||
it('TPLCAT-006: links colours to theme tokens and texts to typeface slots that exist', () => {
|
||||
const tokens: readonly string[] = BOOK_COLOR_TOKENS;
|
||||
const slots: readonly string[] = BOOK_FONT_SLOTS;
|
||||
for (const tpl of BOOK_DECORATED_TEMPLATES) {
|
||||
if (tpl.backgroundToken) expect(tokens, tpl.id).toContain(tpl.backgroundToken);
|
||||
for (const el of tpl.elements) {
|
||||
const links = el as { token?: string; strokeToken?: string; accentToken?: string; fontRole?: string };
|
||||
for (const t of [links.token, links.strokeToken, links.accentToken, el.on]) {
|
||||
if (t !== undefined) expect(tokens, `${tpl.id} ${el.id}`).toContain(t);
|
||||
}
|
||||
if (links.fontRole !== undefined) expect(slots, `${tpl.id} ${el.id}`).toContain(links.fontRole);
|
||||
if (el.kind === 'text' && el.role === 'title') expect(links.fontRole, `${tpl.id} title`).toBeDefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('TPLCAT-007: keeps words inside the safe area, out of the fold, and every frame on the page or bleeding on purpose', () => {
|
||||
const { pageWidth: pw, pageHeight: ph, safe, bleed } = square;
|
||||
for (const tpl of BOOK_DECORATED_TEMPLATES) {
|
||||
const across = tpl.kind === 'inner' ? 2 : 1;
|
||||
for (const { src, laid: out } of laid(tpl)) {
|
||||
if (!out) continue;
|
||||
const b = box(out.el);
|
||||
const where = `${tpl.id} ${src.id}`;
|
||||
// Nothing lies off the page.
|
||||
expect(b.x < pw * across && b.x + b.w > 0 && b.y < ph && b.y + b.h > 0, where).toBe(true);
|
||||
if (out.el.kind === 'text' || out.el.kind === 'badge') {
|
||||
expect(b.x >= safe - 0.01 && b.y >= safe - 0.01, where).toBe(true);
|
||||
expect(b.x + b.w <= pw * across - safe + 0.01 && b.y + b.h <= ph - safe + 0.01, where).toBe(true);
|
||||
}
|
||||
const crosses = across === 2 && b.x < pw - 2 && b.x + b.w > pw + 2;
|
||||
if (!crosses) continue;
|
||||
// Across the fold only a photo or a shape, and only as a band or a picture that runs to the bleed.
|
||||
expect(['photo', 'shape', 'line'], where).toContain(out.el.kind);
|
||||
const bleedsAcross = b.x <= -bleed + 0.01 && b.x + b.w >= pw * 2 + bleed - 0.01;
|
||||
const bleedsDown = b.y <= -bleed + 0.01 && b.y + b.h >= ph + bleed - 0.01;
|
||||
expect(bleedsAcross || bleedsDown, where).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('the lockups of a template', () => {
|
||||
it('TPLCAT-010: groups two or more elements next to each other in paint order, a badge with every word on it', () => {
|
||||
let lockups = 0;
|
||||
for (const tpl of BOOK_DECORATED_TEMPLATES) {
|
||||
const at = new Map<string, number[]>();
|
||||
tpl.elements.forEach((el, i) => {
|
||||
if (el.group) at.set(el.group, [...(at.get(el.group) ?? []), i]);
|
||||
});
|
||||
for (const [group, places] of at) {
|
||||
const where = `${tpl.id} ${group}`;
|
||||
expect(places.length, where).toBeGreaterThanOrEqual(2);
|
||||
expect((places.at(-1) ?? 0) - (places[0] ?? 0) + 1, where).toBe(places.length);
|
||||
}
|
||||
lockups += at.size;
|
||||
}
|
||||
expect(lockups).toBeGreaterThanOrEqual(BOOK_DECORATED_TEMPLATES.length);
|
||||
for (const id of ['grid-badge-circle', 'grid-badge-stamp', 'grid-badge-seal']) {
|
||||
const tpl = findDecoratedTemplate(id) as BookDecoratedTemplate;
|
||||
const words = tpl.elements.filter((e) => e.kind === 'text');
|
||||
expect(
|
||||
words.every((e) => e.group === 'badge' && e.keep === true && e.on !== undefined),
|
||||
id,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('finding the templates', () => {
|
||||
it('TPLCAT-008: finds a template by id and narrows the set by page kind and archetype', () => {
|
||||
expect(findDecoratedTemplate('postcard-houses')?.kind).toBe('single');
|
||||
expect(findDecoratedTemplate('polaroid-stack')?.kind).toBe('inner');
|
||||
expect(findDecoratedTemplate('no-such-template')).toBeUndefined();
|
||||
expect(decoratedTemplatesFor('single').every((t) => t.kind === 'single')).toBe(true);
|
||||
expect(decoratedTemplatesFor('inner', 'postcard').every((t) => t.archetype === 'postcard')).toBe(true);
|
||||
expect(decoratedTemplatesFor('single', null)).toHaveLength(21);
|
||||
expect(isBookTemplateId('grid-badge-circle')).toBe(true);
|
||||
expect(isBookTemplateId('two-up')).toBe(true);
|
||||
expect(isBookTemplateId('cover-full')).toBe(true);
|
||||
expect(isBookTemplateId('nothing-like-it')).toBe(false);
|
||||
});
|
||||
|
||||
it('TPLCAT-009: lists every template for list_book_layouts with its archetype, tags, photo frames and word roles', () => {
|
||||
const row = decoratedTemplateRow(findDecoratedTemplate('hero-trio-wordart') as BookDecoratedTemplate);
|
||||
expect(row).toMatchObject({ id: 'hero-trio-wordart', archetype: 'poster-pop', photoSlots: 4 });
|
||||
expect(row.roles).toEqual(['title', 'date', 'body']);
|
||||
expect(row.tags).toContain('word-art');
|
||||
const all = listBookLayouts(null, square);
|
||||
expect(all.decorated.single).toHaveLength(21);
|
||||
expect(all.decorated.inner).toHaveLength(16);
|
||||
expect(all.notes.decorated).toMatch(/whole designed page/);
|
||||
const inner = listBookLayouts(null, square, 'inner');
|
||||
expect([inner.decorated.single, inner.decorated.inner.length]).toEqual([[], 16]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { BookCoverArchetype } from '../book-archetypes';
|
||||
import { findBookTemplate } from '../book-layouts';
|
||||
import { COVERS_CUTOUT } from './data/covers-cutout.data';
|
||||
import { COVERS_GRID_BADGE } from './data/covers-grid-badge.data';
|
||||
import { COVERS_MINIMAL_SERIF } from './data/covers-minimal-serif.data';
|
||||
import { COVERS_POSTCARD } from './data/covers-postcard.data';
|
||||
import { COVERS_POSTER_POP } from './data/covers-poster-pop.data';
|
||||
import { COVERS_SPINE_BAND } from './data/covers-spine-band.data';
|
||||
import { COVERS_STAMP_COLLAGE } from './data/covers-stamp-collage.data';
|
||||
import { INNER_CHAPTERS } from './data/inner-chapters.data';
|
||||
import { INNER_COLLAGE } from './data/inner-collage.data';
|
||||
import { INNER_EPHEMERA } from './data/inner-ephemera.data';
|
||||
import { INNER_GRIDS } from './data/inner-grids.data';
|
||||
import { INNER_STORY } from './data/inner-story.data';
|
||||
import {
|
||||
templatePhotoSlots,
|
||||
templateRoles,
|
||||
type BookDecoratedTemplate,
|
||||
type BookTemplateKind,
|
||||
} from './template-format';
|
||||
|
||||
/**
|
||||
* The decorated templates the Studio offers and the assistant lays pages
|
||||
* on: 21 covers, three in each of the seven archetypes, and 16 double
|
||||
* spreads, each set in the manner of one of them. The data files hold them
|
||||
* as drawn (template-format.ts); this is the one list the Studio's panel,
|
||||
* the page operations and `list_book_layouts` read.
|
||||
*/
|
||||
export const BOOK_DECORATED_TEMPLATES: readonly BookDecoratedTemplate[] = [
|
||||
...COVERS_SPINE_BAND,
|
||||
...COVERS_POSTER_POP,
|
||||
...COVERS_CUTOUT,
|
||||
...COVERS_POSTCARD,
|
||||
...COVERS_GRID_BADGE,
|
||||
...COVERS_MINIMAL_SERIF,
|
||||
...COVERS_STAMP_COLLAGE,
|
||||
...INNER_STORY,
|
||||
...INNER_CHAPTERS,
|
||||
...INNER_COLLAGE,
|
||||
...INNER_EPHEMERA,
|
||||
...INNER_GRIDS,
|
||||
];
|
||||
|
||||
const BY_ID = new Map(BOOK_DECORATED_TEMPLATES.map((t) => [t.id, t]));
|
||||
|
||||
/** A decorated template by id, or undefined. */
|
||||
export function findDecoratedTemplate(id: string): BookDecoratedTemplate | undefined {
|
||||
return BY_ID.get(id);
|
||||
}
|
||||
|
||||
/** The decorated templates for a single page or a double spread, of one archetype when one is named. */
|
||||
export function decoratedTemplatesFor(
|
||||
kind: BookTemplateKind,
|
||||
archetype?: BookCoverArchetype | null,
|
||||
): BookDecoratedTemplate[] {
|
||||
return BOOK_DECORATED_TEMPLATES.filter((t) => t.kind === kind && (!archetype || t.archetype === archetype));
|
||||
}
|
||||
|
||||
/** Whether a layout id names a template of either set: a built-in layout or a decorated one. */
|
||||
export function isBookTemplateId(id: string): boolean {
|
||||
return BY_ID.has(id) || findBookTemplate(id) !== undefined;
|
||||
}
|
||||
|
||||
/** One decorated template as `list_book_layouts` lists it. */
|
||||
export function decoratedTemplateRow(tpl: BookDecoratedTemplate) {
|
||||
const roles = templateRoles(tpl);
|
||||
return {
|
||||
id: tpl.id,
|
||||
archetype: tpl.archetype,
|
||||
tags: [...tpl.tags],
|
||||
descr: tpl.descr,
|
||||
photoSlots: templatePhotoSlots(tpl),
|
||||
roles: roles.filter((r) => !r.startsWith('photo:')),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { bookPageSetupForPreset } from '../book-pages';
|
||||
import {
|
||||
bookBadgeElementSchema,
|
||||
bookMapElementSchema,
|
||||
bookPhotoElementSchema,
|
||||
bookShapeElementSchema,
|
||||
bookSpreadSchema,
|
||||
bookStickerElementSchema,
|
||||
bookTextElementSchema,
|
||||
type BookElement,
|
||||
type BookSpread,
|
||||
} from '../book.schema';
|
||||
import { bookThemeOf } from '../theme/book-theme';
|
||||
import { layDecoratedTemplate } from './template-apply';
|
||||
import { findDecoratedTemplate } from './template-catalogue';
|
||||
import { bookTemplateFromSpread } from './template-extract';
|
||||
import { bookDecoratedTemplateSchema, type BookDecoratedTemplate } from './template-format';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
// TPLEXT-001 to TPLEXT-005
|
||||
|
||||
/**
|
||||
* A page drawn in the Studio read back as a decorated template: the inverse
|
||||
* of laying one, with the roles read off the layer names, the bindings and the
|
||||
* photo order, the content taken out and the theme's colours kept as tints.
|
||||
*/
|
||||
|
||||
const square = bookPageSetupForPreset('square-210');
|
||||
const landscape = bookPageSetupForPreset('a4-landscape');
|
||||
const meta = { id: 'drawn-one', archetype: 'postcard' as const, tags: ['drawn'], descr: 'A page drawn in the Studio.' };
|
||||
|
||||
let seq = 0;
|
||||
const newId = (p: string) => `${p}-${++seq}`;
|
||||
const frame = { x: 20, y: 30, w: 60, h: 40 };
|
||||
|
||||
const spreadOf = (role: BookSpread['role'], elements: BookElement[], over: Partial<BookSpread> = {}) =>
|
||||
bookSpreadSchema.parse({ id: 'sp', role, elements, ...over });
|
||||
|
||||
/** A catalogue template laid on a page as a person would have drawn it: each role named on its layer. */
|
||||
function drawn(tpl: BookDecoratedTemplate): BookSpread {
|
||||
const laid = layDecoratedTemplate(tpl, square, null, newId);
|
||||
const elements = laid.map(({ el, role }) => (role ? { ...el, name: role } : el));
|
||||
return spreadOf(tpl.kind === 'inner' ? 'inner' : 'cover', elements, {
|
||||
background: tpl.background,
|
||||
...(tpl.backgroundToken ? { backgroundToken: tpl.backgroundToken } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
describe('a drawn page as a template', () => {
|
||||
it('TPLEXT-001: gives back the fractions, the roles and the lengths a template was laid from', () => {
|
||||
for (const id of ['grid-badge-circle', 'stamp-collage-row', 'arch-gallery', 'postcard-back']) {
|
||||
const tpl = findDecoratedTemplate(id) as BookDecoratedTemplate;
|
||||
const back = bookTemplateFromSpread(drawn(tpl), square, { ...meta, id });
|
||||
expect(
|
||||
back.elements.map((e) => e.role),
|
||||
id,
|
||||
).toEqual(tpl.elements.map((e) => e.role));
|
||||
back.elements.forEach((el, i) => {
|
||||
const src = tpl.elements[i] as BookDecoratedTemplate['elements'][number];
|
||||
// A bleeding edge comes back as the bleed of this page: the same edge, laid again.
|
||||
if (src.frame.x > 0 && src.frame.y > 0) {
|
||||
for (const key of ['x', 'y', 'w', 'h'] as const) {
|
||||
expect(el.frame[key], `${id} ${src.id} ${key}`).toBeCloseTo(src.frame[key], 3);
|
||||
}
|
||||
}
|
||||
if (el.kind === 'text' && src.kind === 'text') expect(el.size, `${id} ${src.id}`).toBeCloseTo(src.size, 3);
|
||||
});
|
||||
expect(bookDecoratedTemplateSchema.safeParse(back).success, id).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('TPLEXT-002: reads the roles off the journal bindings, a date mark, a map and the photo order', () => {
|
||||
const els: BookElement[] = [
|
||||
bookPhotoElementSchema.parse({ id: 'a', kind: 'photo', frame, photoId: 5 }),
|
||||
bookTextElementSchema.parse({
|
||||
id: 'b',
|
||||
kind: 'text',
|
||||
frame,
|
||||
text: 'Sintra',
|
||||
binding: { source: 'entry.title', entryId: 2 },
|
||||
}),
|
||||
bookPhotoElementSchema.parse({ id: 'c', kind: 'photo', frame, photoId: 6, name: 'photo:1' }),
|
||||
bookBadgeElementSchema.parse({ id: 'd', kind: 'badge', frame, variant: 'date', text: '4', sub: 'MAY' }),
|
||||
bookMapElementSchema.parse({ id: 'e', kind: 'map', frame, countries: ['PT'] }),
|
||||
bookTextElementSchema.parse({ id: 'f', kind: 'text', frame, text: 'No 7' }),
|
||||
];
|
||||
const tpl = bookTemplateFromSpread(spreadOf('inner', els), square, meta);
|
||||
expect(tpl.kind).toBe('inner');
|
||||
expect(tpl.elements.map((e) => e.role)).toEqual(['photo:2', 'title', 'photo:1', 'date', 'map', undefined]);
|
||||
});
|
||||
|
||||
it('TPLEXT-003: takes the pictures, the words of every role and the journey out, and keeps the words of the design', () => {
|
||||
const els: BookElement[] = [
|
||||
bookPhotoElementSchema.parse({ id: 'a', kind: 'photo', frame, photoId: 5 }),
|
||||
bookTextElementSchema.parse({
|
||||
id: 'b',
|
||||
kind: 'text',
|
||||
frame,
|
||||
text: 'Lisbon',
|
||||
name: 'title',
|
||||
fillPaint: { type: 'photo', photoId: 5 },
|
||||
}),
|
||||
bookTextElementSchema.parse({ id: 'c', kind: 'text', frame, text: '“' }),
|
||||
bookMapElementSchema.parse({ id: 'd', kind: 'map', frame, countries: ['PT'], tileUrl: 'https://tiles' }),
|
||||
bookShapeElementSchema.parse({ id: 'e', kind: 'shape', frame, hidden: true }),
|
||||
];
|
||||
const tpl = bookTemplateFromSpread(spreadOf('cover', els), square, meta);
|
||||
const [picture, title, quote, map] = tpl.elements;
|
||||
expect(tpl.elements).toHaveLength(4);
|
||||
expect(picture).toMatchObject({ kind: 'photo', photoId: null, role: 'photo:1' });
|
||||
expect(title).toMatchObject({ kind: 'text', text: '', binding: null, fillPaint: null, fillPhoto: 'photo:1' });
|
||||
expect(quote).toMatchObject({ kind: 'text', text: '“' });
|
||||
expect(map).toMatchObject({ kind: 'map', countries: [], tileUrl: '', role: 'map' });
|
||||
expect(tpl.kind).toBe('single');
|
||||
expect(tpl.labelKey).toBe('studio.templates.name.drawnOne');
|
||||
});
|
||||
|
||||
it('TPLEXT-004: keeps the colours the document links nowhere as tints of the theme they are drawn in', () => {
|
||||
const theme = bookThemeOf('riviera');
|
||||
const { accent, accent2, paper, ink } = theme.palette;
|
||||
const els: BookElement[] = [
|
||||
bookPhotoElementSchema.parse({ id: 'a', kind: 'photo', frame, border: { width: 3, color: accent } }),
|
||||
bookStickerElementSchema.parse({
|
||||
id: 'b',
|
||||
kind: 'sticker',
|
||||
frame,
|
||||
stickerId: 'lemon',
|
||||
colors: [accent2, '#123456'],
|
||||
}),
|
||||
bookShapeElementSchema.parse({
|
||||
id: 'c',
|
||||
kind: 'shape',
|
||||
frame,
|
||||
paint: { type: 'pattern', pattern: 'checker', fg: accent, bg: paper, scale: 1, angle: 0 },
|
||||
}),
|
||||
];
|
||||
const spread = spreadOf('cover', els, {
|
||||
backgroundPaint: {
|
||||
type: 'linear',
|
||||
angle: 0,
|
||||
stops: [
|
||||
{ at: 0, color: ink },
|
||||
{ at: 1, color: '#abcdef' },
|
||||
],
|
||||
},
|
||||
});
|
||||
const tpl = bookTemplateFromSpread(spread, square, { ...meta, theme });
|
||||
expect(tpl.elements.map((e) => e.tint)).toEqual([
|
||||
{ border: 'accent' },
|
||||
{ colors: ['accent2', null] },
|
||||
{ fg: 'accent', bg: 'paper' },
|
||||
]);
|
||||
expect(tpl.backgroundTint).toEqual({ stops: ['ink', null] });
|
||||
expect(bookTemplateFromSpread(spread, square, meta).elements.every((e) => e.tint === undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('TPLEXT-005: measures a round shape and a turned element from the shorter side of a landscape page', () => {
|
||||
const els: BookElement[] = [
|
||||
bookShapeElementSchema.parse({
|
||||
id: 'a',
|
||||
kind: 'shape',
|
||||
shape: 'ellipse',
|
||||
frame: { x: 100, y: 50, w: 42, h: 42 },
|
||||
}),
|
||||
bookPhotoElementSchema.parse({ id: 'b', kind: 'photo', frame: { x: 10, y: 10, w: 29.7, h: 21 }, rotation: 5 }),
|
||||
bookShapeElementSchema.parse({ id: 'c', kind: 'shape', frame: { x: 29.7, y: 21, w: 59.4, h: 42 } }),
|
||||
];
|
||||
const tpl = bookTemplateFromSpread(spreadOf('cover', els), landscape, meta);
|
||||
const [round, turned, box] = tpl.elements;
|
||||
expect(round).toMatchObject({ keep: true, frame: { w: 0.2, h: 0.2 } });
|
||||
expect(turned?.keep).toBeUndefined();
|
||||
expect(turned?.frame.w).toBeCloseTo(29.7 / 210, 4);
|
||||
expect(box?.frame).toEqual({ x: 0.1, y: 0.1, w: 0.2, h: 0.2 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,234 @@
|
||||
import type { BookCoverArchetype } from '../book-archetypes';
|
||||
import type { BookPaint } from '../book-paint.schema';
|
||||
import type { BookElement, BookPageSetup, BookSpread } from '../book.schema';
|
||||
import { bookThemeColor } from '../theme/book-theme';
|
||||
import { BOOK_COLOR_TOKENS, type BookColorToken, type BookTheme } from '../theme/book-theme.schema';
|
||||
import {
|
||||
BOOK_TEMPLATE_TEXT_ROLES,
|
||||
bookTemplateRoleSchema,
|
||||
templatePhotoIndex,
|
||||
type BookDecoratedTemplate,
|
||||
type BookTemplateElement,
|
||||
type BookTemplatePhotoRole,
|
||||
type BookTemplateRole,
|
||||
type BookTemplateTint,
|
||||
} from './template-format';
|
||||
import { keepsProportions } from './template-lay';
|
||||
import { templateRoleOfBinding } from './template-words';
|
||||
|
||||
/**
|
||||
* A spread drawn in the Studio, turned into a decorated template: the way a
|
||||
* template is made (server/scripts/extract-decorated-templates.cjs reads a
|
||||
* book and writes what this returns into a data file).
|
||||
*
|
||||
* - Every measure is divided back into the fractions the format keeps
|
||||
* (template-format.ts), the inverse of laying a template on a page.
|
||||
* - The pictures come out, and so do the words of every text that takes a
|
||||
* role; a text without one is a word of the design and keeps its words.
|
||||
* - Roles are read off the page. A layer named after a role (`title`,
|
||||
* `photo:2`, `map`) takes it; a text bound to the journal takes the role its
|
||||
* source reads as; a date mark takes `date`, a map `map`; the photo frames
|
||||
* left take the next photo roles in paint order.
|
||||
* - The theme links the document has stay; with the book's theme given, a
|
||||
* colour the document links nowhere (a border, an outline, a pattern's two
|
||||
* colours, a sticker's slots) that is exactly one of the theme's becomes a
|
||||
* tint. A text whose letters a photo of the page fills keeps that photo's
|
||||
* role as `fillPhoto`.
|
||||
*/
|
||||
|
||||
export interface BookTemplateExtract {
|
||||
id: string;
|
||||
archetype: BookCoverArchetype;
|
||||
tags: readonly string[];
|
||||
descr: string;
|
||||
/** The book's theme, for the tints; without one no colour becomes a tint. */
|
||||
theme?: BookTheme | null;
|
||||
}
|
||||
|
||||
const r4 = (v: number) => Math.round(v * 10000) / 10000;
|
||||
|
||||
/** The role a layer's name gives it, when the name is one. */
|
||||
function namedRole(el: BookElement): BookTemplateRole | undefined {
|
||||
const name = el.name?.trim();
|
||||
return name && bookTemplateRoleSchema.safeParse(name).success ? (name as BookTemplateRole) : undefined;
|
||||
}
|
||||
|
||||
/** The role a page's content says an element plays, without a name to say it. */
|
||||
function contentRole(el: BookElement): BookTemplateRole | undefined {
|
||||
if (el.kind === 'text') return templateRoleOfBinding(el.binding);
|
||||
if (el.kind === 'badge' && el.variant === 'date') return 'date';
|
||||
if (el.kind === 'map') return 'map';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Every element's role, each role given once: names first, then the journal, then photo order. */
|
||||
function rolesOf(elements: readonly BookElement[]): (BookTemplateRole | undefined)[] {
|
||||
const taken = new Set<string>();
|
||||
const claim = (role: BookTemplateRole | undefined) => {
|
||||
if (!role || taken.has(role)) return undefined;
|
||||
taken.add(role);
|
||||
return role;
|
||||
};
|
||||
const roles = elements.map((el) => claim(namedRole(el)));
|
||||
elements.forEach((el, i) => {
|
||||
if (!roles[i]) roles[i] = claim(contentRole(el));
|
||||
});
|
||||
let next = 1;
|
||||
elements.forEach((el, i) => {
|
||||
if (roles[i] || el.kind !== 'photo') return;
|
||||
while (taken.has(`photo:${next}`)) next += 1;
|
||||
roles[i] = claim(`photo:${next}`);
|
||||
});
|
||||
return roles;
|
||||
}
|
||||
|
||||
/** The token a colour is exactly, in this theme, or undefined. */
|
||||
function tokenOf(theme: BookTheme | null | undefined, hex: string | null | undefined): BookColorToken | undefined {
|
||||
if (!theme || !hex) return undefined;
|
||||
const want = hex.toLowerCase();
|
||||
return BOOK_COLOR_TOKENS.find((t) => bookThemeColor(theme, t).toLowerCase() === want);
|
||||
}
|
||||
|
||||
type Loose = Record<string, unknown>;
|
||||
type Colour = { color: string } | null | undefined;
|
||||
|
||||
function paintTint(paint: BookPaint | undefined, theme: BookTheme | null | undefined): BookTemplateTint {
|
||||
if (!paint || paint.type === 'solid') return {};
|
||||
if (paint.type === 'pattern') {
|
||||
const fg = tokenOf(theme, paint.fg);
|
||||
const bg = tokenOf(theme, paint.bg);
|
||||
return { ...(fg ? { fg } : {}), ...(bg ? { bg } : {}) };
|
||||
}
|
||||
const stops = paint.stops.map((s) => tokenOf(theme, s.color) ?? null);
|
||||
return stops.some(Boolean) ? { stops } : {};
|
||||
}
|
||||
|
||||
/** The tints of an element: the colours it links nowhere that are its theme's. */
|
||||
function tintOf(el: BookElement, theme: BookTheme | null | undefined): BookTemplateTint | undefined {
|
||||
const of = (value: Colour) => tokenOf(theme, value?.color);
|
||||
let tint: BookTemplateTint = {};
|
||||
if (el.kind === 'photo') tint = { border: of(el.border), shadow: of(el.shadow), cutout: of(el.cutout) };
|
||||
if (el.kind === 'text') {
|
||||
const fill = el.fillPaint?.type === 'solid' ? tokenOf(theme, el.fillPaint.color) : undefined;
|
||||
tint = { outline: of(el.outline), shadow: of(el.shadow), highlight: of(el.highlight), fill };
|
||||
}
|
||||
if (el.kind === 'line') tint = { stroke: tokenOf(theme, el.stroke) };
|
||||
if (el.kind === 'shape') tint = paintTint(el.paint, theme);
|
||||
if (el.kind === 'sticker' && el.colors.length) {
|
||||
const colors = el.colors.map((c) => tokenOf(theme, c) ?? null);
|
||||
if (colors.some(Boolean)) tint = { colors };
|
||||
}
|
||||
const set = Object.entries(tint).filter(([, v]) => v !== undefined);
|
||||
return set.length ? (Object.fromEntries(set) as BookTemplateTint) : undefined;
|
||||
}
|
||||
|
||||
/** Whether an element is round or square on purpose, so it keeps its proportions on a page of another shape. */
|
||||
function keepsShape(el: BookElement): boolean {
|
||||
if (Math.abs(el.frame.w - el.frame.h) > Math.max(el.frame.w, el.frame.h) * 0.01) return false;
|
||||
if (el.kind === 'shape') return el.shape !== 'rect' && el.shape !== 'line';
|
||||
if (el.kind === 'photo') return el.mask !== null && el.mask !== 'rect';
|
||||
return el.kind === 'text' && el.curve?.type === 'circle';
|
||||
}
|
||||
|
||||
/** Every length of an element as a fraction of the page's shorter side. */
|
||||
function lengthsToFractions(out: Loose, unit: number): void {
|
||||
for (const key of ['radius', 'strokeWidth', 'edgeSize', 'size', 'fitMax']) {
|
||||
if (typeof out[key] === 'number') out[key] = r4((out[key] as number) / unit);
|
||||
}
|
||||
for (const key of ['outline', 'border', 'cutout']) {
|
||||
const edge = out[key] as { width: number } | null | undefined;
|
||||
if (edge) out[key] = { ...edge, width: r4(edge.width / unit) };
|
||||
}
|
||||
const shadow = out.shadow as { dx: number; dy: number } | null | undefined;
|
||||
if (shadow) out.shadow = { ...shadow, dx: r4(shadow.dx / unit), dy: r4(shadow.dy / unit) };
|
||||
const band = out.highlight as { padX: number; padY: number; radius: number } | null | undefined;
|
||||
if (band) {
|
||||
out.highlight = { ...band, padX: r4(band.padX / unit), padY: r4(band.padY / unit), radius: r4(band.radius / unit) };
|
||||
}
|
||||
}
|
||||
|
||||
interface Measures {
|
||||
pw: number;
|
||||
ph: number;
|
||||
unit: number;
|
||||
}
|
||||
|
||||
/** A frame as the format keeps it: a kept element's size from the shorter side, about its centre. */
|
||||
function frameToFractions(el: BookElement, keep: boolean, m: Measures) {
|
||||
const f = el.frame;
|
||||
if (!keep) return { x: r4(f.x / m.pw), y: r4(f.y / m.ph), w: r4(f.w / m.pw), h: r4(f.h / m.ph) };
|
||||
const w = f.w / m.unit;
|
||||
const h = f.h / m.unit;
|
||||
return { x: r4((f.x + f.w / 2) / m.pw - w / 2), y: r4((f.y + f.h / 2) / m.ph - h / 2), w: r4(w), h: r4(h) };
|
||||
}
|
||||
|
||||
/** The content taken out of an element that takes a role: its picture, its words, its journey. */
|
||||
function withoutContent(out: Loose, role: BookTemplateRole | undefined): void {
|
||||
if (out.kind === 'photo') out.photoId = null;
|
||||
if (out.kind === 'map')
|
||||
Object.assign(out, { countries: [], points: [], path: [], roads: [], tileUrl: '', attribution: '' });
|
||||
if (out.kind === 'text' && role && (BOOK_TEMPLATE_TEXT_ROLES as readonly string[]).includes(role)) {
|
||||
Object.assign(out, { text: '', binding: null, overridden: false });
|
||||
}
|
||||
delete out.name;
|
||||
delete out.hidden;
|
||||
out.locked = false;
|
||||
}
|
||||
|
||||
/** The photo role whose picture fills a text's letters, when a frame on the page holds that picture. */
|
||||
function fillRole(
|
||||
el: BookElement,
|
||||
elements: readonly BookElement[],
|
||||
roles: readonly (BookTemplateRole | undefined)[],
|
||||
): BookTemplatePhotoRole | undefined {
|
||||
if (el.kind !== 'text' || el.fillPaint?.type !== 'photo') return undefined;
|
||||
const photoId = el.fillPaint.photoId;
|
||||
const i = elements.findIndex((e) => e.kind === 'photo' && e.photoId === photoId);
|
||||
const role = i >= 0 ? roles[i] : undefined;
|
||||
return templatePhotoIndex(role) === null ? undefined : (role as BookTemplatePhotoRole);
|
||||
}
|
||||
|
||||
const camel = (id: string) => id.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase());
|
||||
|
||||
/** A drawn spread as a decorated template, its visible elements in paint order. */
|
||||
export function bookTemplateFromSpread(
|
||||
spread: BookSpread,
|
||||
page: Pick<BookPageSetup, 'pageWidth' | 'pageHeight'>,
|
||||
meta: BookTemplateExtract,
|
||||
): BookDecoratedTemplate {
|
||||
const m: Measures = { pw: page.pageWidth, ph: page.pageHeight, unit: Math.min(page.pageWidth, page.pageHeight) };
|
||||
const visible = spread.elements.filter((e) => !e.hidden);
|
||||
const roles = rolesOf(visible);
|
||||
const elements = visible.map((el, i): BookTemplateElement => {
|
||||
const role = roles[i];
|
||||
const keep = keepsShape(el);
|
||||
const out: Loose = { ...el, id: `${el.kind.charAt(0)}${i + 1}` };
|
||||
out.frame = frameToFractions(el, keep || keepsProportions(el), m);
|
||||
lengthsToFractions(out, m.unit);
|
||||
const fillPhoto = fillRole(el, visible, roles);
|
||||
if (fillPhoto) out.fillPaint = null;
|
||||
withoutContent(out, role);
|
||||
const tint = tintOf(el, meta.theme);
|
||||
return {
|
||||
...(out as BookElement),
|
||||
...(role ? { role } : {}),
|
||||
...(keep ? { keep: true as const } : {}),
|
||||
...(tint ? { tint } : {}),
|
||||
...(fillPhoto ? { fillPhoto } : {}),
|
||||
};
|
||||
});
|
||||
const backgroundTint = paintTint(spread.backgroundPaint, meta.theme);
|
||||
return {
|
||||
id: meta.id,
|
||||
labelKey: `studio.templates.name.${camel(meta.id)}`,
|
||||
kind: spread.role === 'inner' ? 'inner' : 'single',
|
||||
archetype: meta.archetype,
|
||||
tags: [...meta.tags],
|
||||
descr: meta.descr,
|
||||
background: spread.background,
|
||||
...(spread.backgroundToken ? { backgroundToken: spread.backgroundToken } : {}),
|
||||
...(spread.backgroundPaint ? { backgroundPaint: spread.backgroundPaint } : {}),
|
||||
...(Object.keys(backgroundTint).length ? { backgroundTint } : {}),
|
||||
elements,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import { BOOK_COVER_ARCHETYPES, type BookCoverArchetype } from '../book-archetypes';
|
||||
import { MAX_PAINT_STOPS, bookPaintSchema, type BookPaint } from '../book-paint.schema';
|
||||
import { MAX_SPREAD_ELEMENTS, type BookElement } from '../book.schema';
|
||||
import { MAX_STICKER_SLOTS } from '../stickers/sticker-types';
|
||||
import { BOOK_COLOR_TOKENS, type BookColorToken } from '../theme/book-theme.schema';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Decorated templates: whole pages somebody designed, with the pictures and
|
||||
* the words left out, that any page of any book can be laid out on.
|
||||
*
|
||||
* ── The shape ─────────────────────────────────────────────────────────────
|
||||
*
|
||||
* The same as the auto layout's drawn templates (auto/auto-templates.data.ts):
|
||||
* full elements of the document, so a template is drawn in the Studio and read
|
||||
* back out of the book (server/scripts/extract-decorated-templates.cjs), and
|
||||
* every number that is a measure is a fraction of the page it was drawn on.
|
||||
*
|
||||
* - A frame's x and w are fractions of one page's width, y and h of its
|
||||
* height; an inner spread runs from 0 to 2 across. An edge at or past the
|
||||
* trim is a bleed and lands on the bleed of whatever page it is laid on.
|
||||
* - Every other length (a type size in points, a stroke, a radius, a border,
|
||||
* a shadow's offset, an outline, a cut edge) is a fraction of the page's
|
||||
* shorter side, so the type keeps its measure when the page only grows
|
||||
* wider or only taller.
|
||||
* - An element marked `keep`, a turned one and a sticker keep their
|
||||
* proportions: their width and height come from the shorter side too, and
|
||||
* their centre lands where the frame puts it. A circle stays a circle on a
|
||||
* landscape book, and a title set up the spine stays along it.
|
||||
*
|
||||
* ── What a template adds to the elements ────────────────────────────────
|
||||
*
|
||||
* - `role`: what a page's content takes the element. `photo:1` takes the
|
||||
* page's first picture and so on; `title`, `subtitle`, `date`, `body` and
|
||||
* `caption` take its words (a date mark, a badge, takes the date as its
|
||||
* numeral and month); `map` takes its map. An element without a role is
|
||||
* the design and is laid as drawn. `fillPhoto` fills a text's letters with
|
||||
* the picture a photo role took, and `on` names the colour a text is set
|
||||
* on, so a theme whose colours would not read there sets it in ink or
|
||||
* paper, or on a band of one when neither reads.
|
||||
* - Elements that share the document's `group` are one lockup (a title with
|
||||
* its date line, a badge with its words, a stamp with its postmark), laid
|
||||
* as one group of the page that is picked and moved as one.
|
||||
* - The theme links the document already has (`token`, `strokeToken`,
|
||||
* `accentToken`, `fontRole`, a page's `backgroundToken`), kept on the laid
|
||||
* elements, so the page follows the next theme too.
|
||||
* - `tint`: links for the colours the document keeps no link for (a photo's
|
||||
* border, a text's outline, a pattern's two colours, a sticker's slots).
|
||||
* They take the book theme's colours when the template is laid and are not
|
||||
* kept: a person recolouring a sticker later owns that colour.
|
||||
*
|
||||
* Nothing here is a document field, so laying a template writes an ordinary
|
||||
* page that every reader of the format already reads.
|
||||
*/
|
||||
|
||||
/** The words a template's texts can take, by what they say. */
|
||||
export const BOOK_TEMPLATE_TEXT_ROLES = ['title', 'subtitle', 'date', 'body', 'caption'] as const;
|
||||
export type BookTemplateTextRole = (typeof BOOK_TEMPLATE_TEXT_ROLES)[number];
|
||||
|
||||
/** A photo frame's place in the order the page's pictures are poured in, from 1. */
|
||||
export type BookTemplatePhotoRole = `photo:${number}`;
|
||||
export type BookTemplateRole = BookTemplateTextRole | BookTemplatePhotoRole | 'map';
|
||||
|
||||
/** The most photo frames one template pours into. */
|
||||
export const MAX_TEMPLATE_PHOTOS = 12;
|
||||
|
||||
const PHOTO_ROLE = /^photo:([1-9][0-9]?)$/;
|
||||
|
||||
/** The place a photo role pours from, 1 for `photo:1`; null for any other role. */
|
||||
export function templatePhotoIndex(role: string | undefined): number | null {
|
||||
const n = role ? PHOTO_ROLE.exec(role)?.[1] : undefined;
|
||||
return n === undefined ? null : Number(n);
|
||||
}
|
||||
|
||||
/** Colour links a template sets when it is laid, for the colours the document links nowhere. */
|
||||
export interface BookTemplateTint {
|
||||
/** A photo's border, its hard shadow (a text's too) and its cutout. */
|
||||
border?: BookColorToken;
|
||||
shadow?: BookColorToken;
|
||||
cutout?: BookColorToken;
|
||||
/** A text's outline, its marker band and the solid fill of its letters. */
|
||||
outline?: BookColorToken;
|
||||
highlight?: BookColorToken;
|
||||
fill?: BookColorToken;
|
||||
/** A drawn line. */
|
||||
stroke?: BookColorToken;
|
||||
/** A pattern paint's figure and ground, a gradient's stops in order (null keeps one as drawn). */
|
||||
fg?: BookColorToken;
|
||||
bg?: BookColorToken;
|
||||
stops?: readonly (BookColorToken | null)[];
|
||||
/** A sticker's colour slots in order, null keeping a slot's drawn colour. */
|
||||
colors?: readonly (BookColorToken | null)[];
|
||||
}
|
||||
|
||||
/** One element of a template: a document element in fractions, with what the template adds to it. */
|
||||
export type BookTemplateElement = BookElement & {
|
||||
role?: BookTemplateRole;
|
||||
tint?: BookTemplateTint;
|
||||
keep?: true;
|
||||
/** A text whose letters are filled with the picture this photo role takes; without one they keep their colour. */
|
||||
fillPhoto?: BookTemplatePhotoRole;
|
||||
/**
|
||||
* The colour a text is laid on: a band, a block, a badge. Its colour stays
|
||||
* as drawn while it reads there in the book's theme at the size it is fitted
|
||||
* to, else it takes whichever of ink and paper reads better, on a band of
|
||||
* the other when neither does.
|
||||
*/
|
||||
on?: BookColorToken;
|
||||
};
|
||||
|
||||
/** Which pages a template is for: one page (the cover, the first, the last, the back) or a double spread. */
|
||||
export const BOOK_TEMPLATE_KINDS = ['single', 'inner'] as const;
|
||||
export type BookTemplateKind = (typeof BOOK_TEMPLATE_KINDS)[number];
|
||||
|
||||
export interface BookDecoratedTemplate {
|
||||
/** Unique across both template sets, the built-in layouts of book-layouts.ts included. */
|
||||
id: string;
|
||||
/** What the Studio calls it. */
|
||||
labelKey: string;
|
||||
kind: BookTemplateKind;
|
||||
/** The look it belongs to: a cover is one of the archetypes, a spread is set in the manner of one. */
|
||||
archetype: BookCoverArchetype;
|
||||
/** English words for a model and a search: what is on it and what it suits. */
|
||||
tags: readonly string[];
|
||||
/** One sentence of what it looks like, for a model choosing one it cannot see. */
|
||||
descr: string;
|
||||
background: string | null;
|
||||
backgroundToken?: BookColorToken;
|
||||
backgroundPaint?: BookPaint;
|
||||
/** Links for a pattern or gradient page's colours, as an element's `tint` takes them. */
|
||||
backgroundTint?: BookTemplateTint;
|
||||
/** Back to front, as a page paints them. */
|
||||
elements: readonly BookTemplateElement[];
|
||||
}
|
||||
|
||||
/*
|
||||
* ── The contract, for the extract script and the specs ──────────────────
|
||||
*
|
||||
* The element fields themselves are the document's and are held to its
|
||||
* contract once the template is laid on a page (template-lay.ts): in
|
||||
* fractions they are not millimetres yet, so they cannot be parsed as the
|
||||
* document parses them. What is held here is what the template adds.
|
||||
*/
|
||||
|
||||
const token = z.enum(BOOK_COLOR_TOKENS);
|
||||
const hex = z.string().regex(/^#[0-9a-fA-F]{6}$/, 'expected #rrggbb');
|
||||
/** A fraction of a page; an inner spread runs to 2, and a bleeding edge a little past. */
|
||||
const fraction = z.number().finite().min(-0.5).max(2.5);
|
||||
const extent = z.number().finite().positive().max(2.5);
|
||||
|
||||
export const bookTemplateRoleSchema = z.union([
|
||||
z.enum([...BOOK_TEMPLATE_TEXT_ROLES, 'map']),
|
||||
z
|
||||
.string()
|
||||
.regex(PHOTO_ROLE, 'expected photo:n')
|
||||
.refine((r) => (templatePhotoIndex(r) ?? 0) <= MAX_TEMPLATE_PHOTOS, `at most photo:${MAX_TEMPLATE_PHOTOS}`),
|
||||
]);
|
||||
|
||||
export const bookTemplateTintSchema = z.strictObject({
|
||||
border: token.optional(),
|
||||
shadow: token.optional(),
|
||||
cutout: token.optional(),
|
||||
outline: token.optional(),
|
||||
highlight: token.optional(),
|
||||
fill: token.optional(),
|
||||
stroke: token.optional(),
|
||||
fg: token.optional(),
|
||||
bg: token.optional(),
|
||||
stops: z.array(token.nullable()).max(MAX_PAINT_STOPS).optional(),
|
||||
colors: z.array(token.nullable()).max(MAX_STICKER_SLOTS).optional(),
|
||||
});
|
||||
|
||||
export const bookTemplateElementSchema = z.looseObject({
|
||||
id: z.string().min(1).max(40),
|
||||
kind: z.string().min(1),
|
||||
frame: z.strictObject({ x: fraction, y: fraction, w: extent, h: extent }),
|
||||
role: bookTemplateRoleSchema.optional(),
|
||||
tint: bookTemplateTintSchema.optional(),
|
||||
keep: z.literal(true).optional(),
|
||||
fillPhoto: z.string().regex(PHOTO_ROLE, 'expected photo:n').optional(),
|
||||
on: token.optional(),
|
||||
});
|
||||
|
||||
export const bookDecoratedTemplateSchema = z.strictObject({
|
||||
id: z.string().regex(/^[a-z][a-z0-9-]{2,39}$/, 'expected a lower-case id'),
|
||||
labelKey: z.string().regex(/^studio\.templates\.name\.[a-zA-Z0-9]+$/),
|
||||
kind: z.enum(BOOK_TEMPLATE_KINDS),
|
||||
archetype: z.enum(BOOK_COVER_ARCHETYPES),
|
||||
tags: z
|
||||
.array(z.string().regex(/^[a-z][a-z0-9-]*$/))
|
||||
.min(1)
|
||||
.max(12),
|
||||
descr: z.string().min(10).max(240),
|
||||
background: hex.nullable(),
|
||||
backgroundToken: token.optional(),
|
||||
backgroundPaint: bookPaintSchema.optional(),
|
||||
backgroundTint: bookTemplateTintSchema.optional(),
|
||||
elements: z.array(bookTemplateElementSchema).min(1).max(MAX_SPREAD_ELEMENTS),
|
||||
});
|
||||
|
||||
/** The roles a template's elements carry, in paint order. */
|
||||
export function templateRoles(tpl: Pick<BookDecoratedTemplate, 'elements'>): BookTemplateRole[] {
|
||||
return tpl.elements.flatMap((e) => (e.role ? [e.role] : []));
|
||||
}
|
||||
|
||||
/** How many pictures a template pours: its photo frames that take one. */
|
||||
export function templatePhotoSlots(tpl: Pick<BookDecoratedTemplate, 'elements'>): number {
|
||||
return tpl.elements.filter((e) => e.kind === 'photo' && templatePhotoIndex(e.role) !== null).length;
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import { contrastRatio, requiredContrast } from '../book-colour';
|
||||
import { withShapePaint } from '../book-paint';
|
||||
import type { BookPaint } from '../book-paint.schema';
|
||||
import { bookTextHasOwnGround } from '../book-text-findings';
|
||||
import {
|
||||
bookElementSchema,
|
||||
type BookElement,
|
||||
type BookFrame,
|
||||
type BookPageSetup,
|
||||
type BookTextElement,
|
||||
} from '../book.schema';
|
||||
import { bookThemeColor } from '../theme/book-theme';
|
||||
import type { BookColorToken, BookTheme } from '../theme/book-theme.schema';
|
||||
import type {
|
||||
BookDecoratedTemplate,
|
||||
BookTemplateElement,
|
||||
BookTemplatePhotoRole,
|
||||
BookTemplateRole,
|
||||
BookTemplateTint,
|
||||
} from './template-format';
|
||||
|
||||
/**
|
||||
* A template's fractions turned into millimetres on a page (template-format.ts
|
||||
* says what each number is a fraction of), its tints into the theme's colours,
|
||||
* and every element held to the document's contract on the way out.
|
||||
*/
|
||||
|
||||
const r2 = (v: number) => Math.round(v * 100) / 100;
|
||||
const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));
|
||||
|
||||
/** The measures of a page a template is laid on: one page's width and height, the shorter side, the bleed, how many pages across. */
|
||||
export function templatePage(page: BookPageSetup, across: 1 | 2) {
|
||||
return {
|
||||
pw: page.pageWidth,
|
||||
ph: page.pageHeight,
|
||||
/** Every length but a frame's is a fraction of this. */
|
||||
unit: Math.min(page.pageWidth, page.pageHeight),
|
||||
bleed: page.bleed,
|
||||
across,
|
||||
};
|
||||
}
|
||||
type TemplatePage = ReturnType<typeof templatePage>;
|
||||
|
||||
/** Whether an element keeps its proportions when the page is not the shape it was drawn on. */
|
||||
export function keepsProportions(el: Pick<BookTemplateElement, 'keep' | 'rotation' | 'kind'>): boolean {
|
||||
return el.keep === true || el.rotation % 360 !== 0 || el.kind === 'sticker';
|
||||
}
|
||||
|
||||
/**
|
||||
* One axis of a frame in millimetres. An edge at or past the trim of the
|
||||
* sheet is a bleed, and lands on this page's bleed whatever the template's
|
||||
* page bled: three millimetres of a 210 mm page are not three of a 148 mm one.
|
||||
*/
|
||||
function axis(start: number, size: number, scale: number, pages: number, bleed: number): [number, number] {
|
||||
const end = start + size;
|
||||
const from = start <= 0 ? -bleed : start * scale;
|
||||
const to = end >= pages ? pages * scale + bleed : end * scale;
|
||||
return [from, Math.max(0.5, to - from)];
|
||||
}
|
||||
|
||||
/** A frame in millimetres on the page. */
|
||||
function templateFrame(el: BookTemplateElement, at: TemplatePage): BookFrame {
|
||||
const f = el.frame;
|
||||
if (keepsProportions(el)) {
|
||||
const w = f.w * at.unit;
|
||||
const h = f.h * at.unit;
|
||||
const cx = (f.x + f.w / 2) * at.pw;
|
||||
const cy = (f.y + f.h / 2) * at.ph;
|
||||
return { x: r2(cx - w / 2), y: r2(cy - h / 2), w: r2(w), h: r2(h) };
|
||||
}
|
||||
const [x, w] = axis(f.x, f.w, at.pw, at.across, at.bleed);
|
||||
const [y, h] = axis(f.y, f.h, at.ph, 1, at.bleed);
|
||||
return { x: r2(x), y: r2(y), w: r2(w), h: r2(h) };
|
||||
}
|
||||
|
||||
/** A length of the template in millimetres (or points, for a type size). */
|
||||
const len = (v: number, at: TemplatePage) => r2(v * at.unit);
|
||||
|
||||
/** An offset a shadow is moved by, kept inside the reach the document allows. */
|
||||
const reach = (v: number, at: TemplatePage) => clamp(len(v, at), -30, 30);
|
||||
|
||||
type Loose = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* The smallest a template's type is set at on a page smaller than it was drawn
|
||||
* for: running text from 7 pt, a caption or a date line from 6 pt, the sizes a
|
||||
* printed page still reads at. Larger type scales with the page.
|
||||
*/
|
||||
const TYPE_FLOOR = { body: 7, line: 6 } as const;
|
||||
|
||||
function scaleText(el: Loose, at: TemplatePage, role: BookTemplateRole | undefined): void {
|
||||
const floor = role === 'body' ? TYPE_FLOOR.body : TYPE_FLOOR.line;
|
||||
const sized = (v: number) => clamp(Math.max(len(v, at), floor), 4, 400);
|
||||
if (typeof el.size === 'number') el.size = sized(el.size);
|
||||
if (typeof el.fitMax === 'number') el.fitMax = sized(el.fitMax);
|
||||
const outline = el.outline as { width: number } | null | undefined;
|
||||
if (outline) el.outline = { ...outline, width: clamp(len(outline.width, at), 0.05, 6) };
|
||||
const shadow = el.shadow as { dx: number; dy: number } | null | undefined;
|
||||
if (shadow) el.shadow = { ...shadow, dx: reach(shadow.dx, at), dy: reach(shadow.dy, at) };
|
||||
const band = el.highlight as { padX: number; padY: number; radius: number } | null | undefined;
|
||||
if (band) {
|
||||
el.highlight = {
|
||||
...band,
|
||||
padX: clamp(len(band.padX, at), 0, 20),
|
||||
padY: clamp(len(band.padY, at), 0, 20),
|
||||
radius: clamp(len(band.radius, at), 0, 20),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function scalePhoto(el: Loose, at: TemplatePage): void {
|
||||
for (const key of ['border', 'cutout'] as const) {
|
||||
const edge = el[key] as { width: number } | null | undefined;
|
||||
if (edge) el[key] = { ...edge, width: clamp(len(edge.width, at), 0.1, 30) };
|
||||
}
|
||||
const shadow = el.shadow as { dx: number; dy: number } | null | undefined;
|
||||
if (shadow) el.shadow = { ...shadow, dx: reach(shadow.dx, at), dy: reach(shadow.dy, at) };
|
||||
}
|
||||
|
||||
/** Every length of an element scaled from its fraction, kept inside the document's ranges. */
|
||||
function scaleLengths(el: Loose, at: TemplatePage, role: BookTemplateRole | undefined): void {
|
||||
if (typeof el.radius === 'number') el.radius = Math.max(0, len(el.radius, at));
|
||||
if (typeof el.strokeWidth === 'number') {
|
||||
el.strokeWidth =
|
||||
el.kind === 'line' ? clamp(len(el.strokeWidth, at), 0.05, 20) : Math.max(0, len(el.strokeWidth, at));
|
||||
}
|
||||
if (typeof el.edgeSize === 'number') el.edgeSize = clamp(len(el.edgeSize, at), 0.5, 30);
|
||||
if (el.kind === 'text') scaleText(el, at, role);
|
||||
if (el.kind === 'photo') scalePhoto(el, at);
|
||||
}
|
||||
|
||||
/*
|
||||
* ── Tints ───────────────────────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
type Colour = (token: BookColorToken) => string;
|
||||
|
||||
/** A colour object's `color` set from a token, the object kept as it is without one. */
|
||||
function tinted<T extends { color: string }>(
|
||||
value: T | null | undefined,
|
||||
token: BookColorToken | undefined,
|
||||
colour: Colour,
|
||||
) {
|
||||
return value && token ? { ...value, color: colour(token) } : value;
|
||||
}
|
||||
|
||||
/** A paint's colours set from its tints. */
|
||||
function tintedPaint(paint: BookPaint, tint: BookTemplateTint, colour: Colour): BookPaint {
|
||||
if (paint.type === 'pattern') {
|
||||
return {
|
||||
...paint,
|
||||
...(tint.fg ? { fg: colour(tint.fg) } : {}),
|
||||
...(tint.bg && paint.bg !== null ? { bg: colour(tint.bg) } : {}),
|
||||
};
|
||||
}
|
||||
if (paint.type === 'solid') return tint.fill ? { ...paint, color: colour(tint.fill) } : paint;
|
||||
const stops = tint.stops;
|
||||
if (!stops) return paint;
|
||||
return {
|
||||
...paint,
|
||||
stops: paint.stops.map((s, i) => {
|
||||
const t = stops[i];
|
||||
return t ? { ...s, color: colour(t) } : s;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function tintPhoto(el: Loose, tint: BookTemplateTint, colour: Colour): void {
|
||||
el.border = tinted(el.border as { color: string } | null | undefined, tint.border, colour);
|
||||
el.shadow = tinted(el.shadow as { color: string } | null | undefined, tint.shadow, colour);
|
||||
el.cutout = tinted(el.cutout as { color: string } | null | undefined, tint.cutout, colour);
|
||||
}
|
||||
|
||||
function tintText(el: Loose, tint: BookTemplateTint, colour: Colour): void {
|
||||
el.outline = tinted(el.outline as { color: string } | null | undefined, tint.outline, colour);
|
||||
el.shadow = tinted(el.shadow as { color: string } | null | undefined, tint.shadow, colour);
|
||||
el.highlight = tinted(el.highlight as { color: string } | null | undefined, tint.highlight, colour);
|
||||
const fill = el.fillPaint as { type: string; color?: string } | null | undefined;
|
||||
if (fill?.type === 'solid' && tint.fill) el.fillPaint = { ...fill, color: colour(tint.fill) };
|
||||
}
|
||||
|
||||
function tintSticker(el: Loose, tint: BookTemplateTint, colour: Colour): void {
|
||||
if (!tint.colors) return;
|
||||
const drawn = (el.colors as string[] | undefined) ?? [];
|
||||
el.colors = drawn.map((c, i) => {
|
||||
const t = tint.colors?.[i];
|
||||
return t ? colour(t) : c;
|
||||
});
|
||||
}
|
||||
|
||||
/** The colours a template links nowhere in the document, set from the theme. Changes `el` in place. */
|
||||
function applyTint(el: Loose, tint: BookTemplateTint, theme: BookTheme): void {
|
||||
const colour: Colour = (t) => bookThemeColor(theme, t);
|
||||
if (el.kind === 'photo') tintPhoto(el, tint, colour);
|
||||
else if (el.kind === 'text') tintText(el, tint, colour);
|
||||
else if (el.kind === 'sticker') tintSticker(el, tint, colour);
|
||||
else if (el.kind === 'line' && tint.stroke) el.stroke = colour(tint.stroke);
|
||||
}
|
||||
|
||||
/** The band set behind a text that reads on neither ink nor paper: its padding as a share of the letter height. */
|
||||
const READING_BAND = { x: 0.6, y: 0.3 } as const;
|
||||
|
||||
/**
|
||||
* A laid text on the colour `on`, set so it reads there in this theme at the
|
||||
* size it was fitted to. Its own colour stays while it reads; else it takes
|
||||
* whichever of ink and paper reads better, and a hard shadow that would vanish
|
||||
* into the new colour takes the old one; when neither reads, the better of the
|
||||
* two goes on a band of the other. A text with its own ground (a band, a
|
||||
* picture in its letters, a heavy outline) already reads on it.
|
||||
*/
|
||||
export function readableOn(el: BookTextElement, on: BookColorToken, theme: BookTheme): BookTextElement {
|
||||
if (bookTextHasOwnGround(el)) return el;
|
||||
const ground = bookThemeColor(theme, on);
|
||||
const need = requiredContrast(el.size);
|
||||
if (contrastRatio(el.color, ground) >= need) return el;
|
||||
const ink = bookThemeColor(theme, 'ink');
|
||||
const paper = bookThemeColor(theme, 'paper');
|
||||
const pick: BookColorToken = contrastRatio(ink, ground) >= contrastRatio(paper, ground) ? 'ink' : 'paper';
|
||||
const color = pick === 'ink' ? ink : paper;
|
||||
const next: BookTextElement = { ...el, token: pick, color };
|
||||
if (el.shadow?.color.toLowerCase() === color.toLowerCase()) {
|
||||
next.shadow = { ...el.shadow, color: el.color };
|
||||
}
|
||||
if (contrastRatio(color, ground) >= need) return next;
|
||||
const letter = (el.size * 25.4) / 72;
|
||||
const pad = (share: number) => clamp(r2(letter * share), 0, 20);
|
||||
const band = pick === 'ink' ? paper : ink;
|
||||
return {
|
||||
...next,
|
||||
highlight: { color: band, alpha: 1, padX: pad(READING_BAND.x), padY: pad(READING_BAND.y), radius: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
/** A laid element, with what the template says about it kept beside it for the pour. */
|
||||
export interface LaidElement {
|
||||
el: BookElement;
|
||||
role: BookTemplateRole | undefined;
|
||||
fillPhoto: BookTemplatePhotoRole | undefined;
|
||||
/** The colour a text is set on, for `readableOn` once its words are in and fitted. */
|
||||
on: BookColorToken | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* One template element on this page: in millimetres, its tints in the theme's
|
||||
* colours when the book has a theme, its template fields gone, parsed as the
|
||||
* document parses an element. Null for one the contract refuses, which a
|
||||
* shipped template never is (a spec holds them) and a drawn one may be.
|
||||
*/
|
||||
export function layTemplateElement(
|
||||
src: BookTemplateElement,
|
||||
at: TemplatePage,
|
||||
theme: BookTheme | null,
|
||||
id: string,
|
||||
): LaidElement | null {
|
||||
const { role, tint, keep: _keep, fillPhoto, on, ...rest } = src;
|
||||
const el: Loose = { ...rest, id, frame: templateFrame(src, at) };
|
||||
scaleLengths(el, at, role);
|
||||
if (tint && theme) applyTint(el, tint, theme);
|
||||
const parsed = bookElementSchema.safeParse(el);
|
||||
if (!parsed.success) return null;
|
||||
let out = parsed.data;
|
||||
// A shape whose paint took the theme keeps its fill the colour that stands for it.
|
||||
if (out.kind === 'shape' && out.paint && tint && theme) {
|
||||
out = withShapePaint(
|
||||
out,
|
||||
tintedPaint(out.paint, tint, (t) => bookThemeColor(theme, t)),
|
||||
);
|
||||
}
|
||||
return { el: out, role, fillPhoto, on: out.kind === 'text' ? on : undefined };
|
||||
}
|
||||
|
||||
/** The page's own paint, its tints taken from the theme. */
|
||||
export function templateBackgroundPaint(tpl: BookDecoratedTemplate, theme: BookTheme | null): BookPaint | undefined {
|
||||
const paint = tpl.backgroundPaint;
|
||||
if (!paint || !theme || !tpl.backgroundTint) return paint;
|
||||
return tintedPaint(paint, tpl.backgroundTint, (t) => bookThemeColor(theme, t));
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { applyBookOps } from '../book-ops';
|
||||
import type { BookOpsContext } from '../book-ops-core';
|
||||
import { bookPageOpSchema } from '../book-ops.schema';
|
||||
import { blankBook } from '../book-pages';
|
||||
import {
|
||||
bookPhotoElementSchema,
|
||||
bookShapeElementSchema,
|
||||
type BookDocument,
|
||||
type BookElement,
|
||||
type BookSpread,
|
||||
} from '../book.schema';
|
||||
import { bookThemeOf } from '../theme/book-theme';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
// TPLOP-001 to TPLOP-007
|
||||
|
||||
/**
|
||||
* The page operation `layout` with a decorated template, as the assistant's
|
||||
* edit_book_page and revise_book_design send it: the same apply the Studio
|
||||
* runs, given the journey's words and the book's theme, refused on the other
|
||||
* kind of page, and an id the catalogue does not hold refused by the schema.
|
||||
*/
|
||||
|
||||
let seq = 0;
|
||||
const newId = (p: string) => `${p}-o${++seq}`;
|
||||
|
||||
const photo = (id: string, photoId: number) =>
|
||||
bookPhotoElementSchema.parse({ id, kind: 'photo', frame: { x: 10, y: 10, w: 40, h: 30 }, photoId });
|
||||
|
||||
function book(cover: BookElement[], inner: BookElement[] = [], over: Partial<BookDocument> = {}): BookDocument {
|
||||
const doc = blankBook({ title: 'Portugal' });
|
||||
const ids = ['sp-cover', 'sp-first', 'sp-in', 'sp-last', 'sp-back'];
|
||||
const spreads = doc.spreads.map((s, i) => ({ ...s, id: ids[i] as string }));
|
||||
spreads[0] = { ...(spreads[0] as BookSpread), elements: cover };
|
||||
spreads[2] = { ...(spreads[2] as BookSpread), elements: inner, entryId: 9 };
|
||||
return { ...doc, spreads, ...over };
|
||||
}
|
||||
|
||||
const run = (doc: BookDocument, spreadId: string, template: string, ctx: BookOpsContext = {}) =>
|
||||
applyBookOps(doc, { spreadId, ops: [{ op: 'layout', template }] }, { newId, ...ctx });
|
||||
|
||||
function laid(result: ReturnType<typeof applyBookOps>, index: number): BookSpread {
|
||||
if (!result.ok) throw new Error(`refused: ${result.error.code} ${result.error.message}`);
|
||||
return result.doc.spreads[index] as BookSpread;
|
||||
}
|
||||
|
||||
const texts = (spread: BookSpread) => spread.elements.flatMap((e) => (e.kind === 'text' ? [e.text] : []));
|
||||
|
||||
describe('the layout operation with a decorated template', () => {
|
||||
it('TPLOP-001: lays the cover out on a decorated template, in the book title, its photo poured in', () => {
|
||||
const cover = laid(run(book([photo('c1', 12)]), 'sp-cover', 'minimal-serif-classic'), 0);
|
||||
expect(texts(cover)).toContain('Portugal');
|
||||
expect(cover.elements.find((e) => e.kind === 'photo')).toMatchObject({ photoId: 12 });
|
||||
expect(cover.elements.find((e) => e.kind === 'line')).toMatchObject({ ornament: 'sun' });
|
||||
});
|
||||
|
||||
it('TPLOP-002: sets a page made from an entry in the words of the journal entry, bound to it', () => {
|
||||
const ctx: BookOpsContext = {
|
||||
journey: {
|
||||
title: 'Portugal',
|
||||
entries: [{ id: 9, title: 'Sintra', story: 'Up through the fog.', location: null }],
|
||||
},
|
||||
};
|
||||
const inner = laid(run(book([], [photo('i1', 13)]), 'sp-in', 'story-quote', ctx), 2);
|
||||
const title = inner.elements.find((e) => e.kind === 'text' && e.text === 'Sintra');
|
||||
expect(title?.kind === 'text' && title.binding).toEqual({ source: 'entry.title', entryId: 9 });
|
||||
expect(texts(inner)).toContain('Up through the fog.');
|
||||
});
|
||||
|
||||
it('TPLOP-003: sets the page in the book theme', () => {
|
||||
const theme = bookThemeOf('alpine');
|
||||
const cover = laid(run(book([], [], { theme }), 'sp-cover', 'postcard-houses'), 0);
|
||||
const title = cover.elements.find((e) => e.kind === 'text' && e.text === 'Portugal');
|
||||
expect(title).toMatchObject({ color: theme.palette.ink, font: theme.fonts.display.font });
|
||||
expect(cover.background).toBe(theme.palette.paper);
|
||||
});
|
||||
|
||||
it('TPLOP-004: refuses a cover template on a double spread and a spread template on a cover', () => {
|
||||
const wrong = run(book([]), 'sp-in', 'poster-pop-sunburst');
|
||||
expect(wrong).toMatchObject({ ok: false, error: { code: 'layout_role', opIndex: 0 } });
|
||||
expect(run(book([]), 'sp-cover', 'grid-tight')).toMatchObject({ ok: false, error: { code: 'layout_role' } });
|
||||
});
|
||||
|
||||
it('TPLOP-006: keeps the locked elements of a cover and the name of their group, as the Studio does', () => {
|
||||
const locked = (id: string) =>
|
||||
bookShapeElementSchema.parse({
|
||||
id,
|
||||
kind: 'shape',
|
||||
frame: { x: 2, y: 2, w: 6, h: 6 },
|
||||
locked: true,
|
||||
group: 'g-keep',
|
||||
});
|
||||
const doc = book([locked('k1'), locked('k2'), photo('c1', 12)]);
|
||||
const named = {
|
||||
...doc,
|
||||
spreads: doc.spreads.map((s, i) => (i === 0 ? { ...s, groups: [{ id: 'g-keep', name: 'My lockup' }] } : s)),
|
||||
};
|
||||
const cover = laid(run(named, 'sp-cover', 'minimal-serif-classic'), 0);
|
||||
expect(cover.elements.filter((e) => e.group === 'g-keep').map((e) => e.id)).toEqual(['k1', 'k2']);
|
||||
expect(cover.groups).toContainEqual({ id: 'g-keep', name: 'My lockup' });
|
||||
});
|
||||
|
||||
it("TPLOP-007: sets a cover in the journey's subtitle, bound to it, as the Studio's panel does", () => {
|
||||
const ctx: BookOpsContext = { journey: { title: 'Azores', subtitle: 'Nine islands in the Atlantic', entries: [] } };
|
||||
const cover = laid(run(book([]), 'sp-cover', 'minimal-serif-classic', ctx), 0);
|
||||
const subtitle = cover.elements.find((e) => e.kind === 'text' && e.text === 'Nine islands in the Atlantic');
|
||||
expect(subtitle?.kind === 'text' && subtitle.binding).toEqual({ source: 'journey.subtitle' });
|
||||
});
|
||||
|
||||
it('TPLOP-005: takes any id of either set as a plain string and refuses one the catalogue does not hold', () => {
|
||||
expect(bookPageOpSchema.safeParse({ op: 'layout', template: 'postcard-coast' }).success).toBe(true);
|
||||
expect(bookPageOpSchema.safeParse({ op: 'layout', template: 'two-up' }).success).toBe(true);
|
||||
const unknown = bookPageOpSchema.safeParse({ op: 'layout', template: 'made-up-layout' });
|
||||
expect(unknown.success).toBe(false);
|
||||
expect(run(book([]), 'sp-in', 'made-up-layout')).toMatchObject({ ok: false, error: { code: 'invalid_op' } });
|
||||
const builtIn = laid(run(book([]), 'sp-in', 'quiet-text'), 2);
|
||||
expect(builtIn.elements.every((e) => e.kind === 'text' || e.kind === 'photo')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { failOp, type BookOpsContext } from '../book-ops-core';
|
||||
import type { BookDocument, BookPageSetup, BookSpread } from '../book.schema';
|
||||
import { applyDecoratedTemplate } from './template-apply';
|
||||
import type { BookDecoratedTemplate } from './template-format';
|
||||
import { bookTemplateWords, type BookTemplateWordSource } from './template-words';
|
||||
|
||||
/**
|
||||
* The page operation `layout` with a decorated template: the same apply the
|
||||
* Studio's templates panel runs, given the words and the figures the book
|
||||
* engine knows, so a page the assistant lays out on a template is the page a
|
||||
* person would get by clicking it.
|
||||
*/
|
||||
|
||||
interface DecoratedLayoutEnv {
|
||||
doc: BookDocument;
|
||||
page: BookPageSetup;
|
||||
ctx: BookOpsContext;
|
||||
newId: (prefix: string) => string;
|
||||
opIndex: number;
|
||||
}
|
||||
|
||||
/** What the engine knows of the journey's words: the journal entries with the dates the travel figures carry. */
|
||||
function templateWordSourceOf(doc: BookDocument, ctx: BookOpsContext): BookTemplateWordSource {
|
||||
const travel = ctx.travel?.entries ?? [];
|
||||
const dates = new Map(travel.map((e) => [e.id, e.date]));
|
||||
const entries = (ctx.journey?.entries ?? travel).map((e) => ({
|
||||
id: e.id,
|
||||
title: e.title ?? null,
|
||||
story: e.story ?? null,
|
||||
location: e.location ?? null,
|
||||
// A date is set in the book's language, which only the travel context knows.
|
||||
date: ctx.travel ? (dates.get(e.id) ?? null) : null,
|
||||
}));
|
||||
return {
|
||||
bookTitle: doc.title.trim() || ctx.journey?.title || '',
|
||||
subtitle: ctx.journey?.subtitle ?? null,
|
||||
locale: ctx.travel?.locale ?? 'en',
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
/** The spread laid out on a decorated template, refused when the template is for the other kind of page. */
|
||||
export function opDecoratedLayout(spread: BookSpread, tpl: BookDecoratedTemplate, env: DecoratedLayoutEnv): BookSpread {
|
||||
const single = spread.role !== 'inner';
|
||||
if ((tpl.kind === 'single') !== single) {
|
||||
throw failOp(
|
||||
'layout_role',
|
||||
`${tpl.id} is a ${tpl.kind === 'single' ? 'single-page (cover) template' : 'double-spread template'}; this page is ${single ? 'a single page' : 'a double spread'}`,
|
||||
env.opIndex,
|
||||
);
|
||||
}
|
||||
return applyDecoratedTemplate(spread, tpl, env.page, {
|
||||
theme: env.doc.theme ?? null,
|
||||
words: bookTemplateWords(spread, templateWordSourceOf(env.doc, env.ctx)),
|
||||
travel: env.ctx.travel ?? null,
|
||||
...(env.ctx.travel ? { locale: env.ctx.travel.locale } : {}),
|
||||
newId: env.newId,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { bookSpreadSchema, bookTextElementSchema, type BookSpread, type BookTextElement } from '../book.schema';
|
||||
import type { BookTemplateTextRole } from './template-format';
|
||||
import { assignTemplateTexts, bookTemplateWords } from './template-words';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
// TPLWORDS-001 to TPLWORDS-006
|
||||
|
||||
/**
|
||||
* Which of a page's words a template's roles take: the journal's bindings
|
||||
* first, then the largest type as the title, a line that says a date as the
|
||||
* date, a long text only as the story; and the words a role takes from the
|
||||
* book when the page has none.
|
||||
*/
|
||||
|
||||
const text = (id: string, words: string, over: Partial<BookTextElement> = {}) =>
|
||||
bookTextElementSchema.parse({ id, kind: 'text', frame: { x: 0, y: 0, w: 100, h: 20 }, text: words, ...over });
|
||||
|
||||
const ALL = new Set<BookTemplateTextRole>(['title', 'subtitle', 'date', 'body', 'caption']);
|
||||
const story = 'A long day on the water, from the harbour at first light to the islands and back. '.repeat(3);
|
||||
|
||||
const ids = (m: Map<BookTemplateTextRole, BookTextElement>) =>
|
||||
Object.fromEntries([...m].map(([role, el]) => [role, el.id]));
|
||||
|
||||
const page = (over: Partial<BookSpread>) => bookSpreadSchema.parse({ id: 'sp', ...over });
|
||||
|
||||
describe('the page words in the roles', () => {
|
||||
it('TPLWORDS-001: gives a bound text the role its source reads as, whatever its size', () => {
|
||||
const got = assignTemplateTexts(
|
||||
[
|
||||
text('loc', 'Hvar', { size: 40, binding: { source: 'entry.location', entryId: 1 } }),
|
||||
text('title', 'Islands', { size: 9, binding: { source: 'entry.title', entryId: 1 } }),
|
||||
text('day', 'Tuesday', { binding: { source: 'entry.date', entryId: 1 } }),
|
||||
text('cap', 'The harbour', { binding: { source: 'photo.caption', photoId: 3 } }),
|
||||
],
|
||||
ALL,
|
||||
);
|
||||
expect(ids(got)).toEqual({ subtitle: 'loc', title: 'title', date: 'day', caption: 'cap' });
|
||||
});
|
||||
|
||||
it('TPLWORDS-002: reads unbound words as a title, a date line and a story', () => {
|
||||
const got = assignTemplateTexts(
|
||||
[text('a', story), text('b', 'Croatia', { size: 30 }), text('c', '12.08.2025')],
|
||||
ALL,
|
||||
);
|
||||
expect(ids(got)).toEqual({ title: 'b', body: 'a', date: 'c' });
|
||||
});
|
||||
|
||||
it('TPLWORDS-003: makes the first short line the title when nothing is set large, and a line without a date a subtitle', () => {
|
||||
const got = assignTemplateTexts([text('a', 'Split'), text('b', 'The old town')], ALL);
|
||||
expect(ids(got)).toEqual({ title: 'a', subtitle: 'b' });
|
||||
});
|
||||
|
||||
it('TPLWORDS-004: never sets running text in a short role, and leaves out what no role takes', () => {
|
||||
const got = assignTemplateTexts([text('a', story), text('b', 'Croatia', { size: 30 })], new Set(['title', 'date']));
|
||||
expect(ids(got)).toEqual({ title: 'b' });
|
||||
const spare = assignTemplateTexts([text('a', 'One'), text('b', 'Two'), text('c', 'Three')], new Set(['title']));
|
||||
expect(ids(spare)).toEqual({ title: 'a' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('the book words for the roles a page has none for', () => {
|
||||
const entries = [
|
||||
{ id: 4, title: 'Sintra', location: 'Serra de Sintra', date: '2026-05-14', story: 'Fog, then sun.' },
|
||||
];
|
||||
|
||||
it('TPLWORDS-005: sets a cover in the book title, the journey subtitle and the year of its first dated stop', () => {
|
||||
const words = bookTemplateWords(page({ role: 'cover' }), {
|
||||
bookTitle: ' Portugal ',
|
||||
subtitle: 'Lisbon to the Algarve',
|
||||
locale: 'en',
|
||||
entries: [{ id: 1, date: null }, ...entries],
|
||||
});
|
||||
expect(words).toEqual({
|
||||
title: { text: 'Portugal' },
|
||||
subtitle: { text: 'Lisbon to the Algarve', binding: { source: 'journey.subtitle' } },
|
||||
date: { text: '2026' },
|
||||
});
|
||||
expect(bookTemplateWords(page({ role: 'back' }), { bookTitle: ' ', locale: 'en' })).toEqual({});
|
||||
});
|
||||
|
||||
it('TPLWORDS-006: sets a page made from an entry in its words, bound to it, and a page from none in nothing', () => {
|
||||
const words = bookTemplateWords(page({ role: 'inner', entryId: 4 }), {
|
||||
bookTitle: 'Portugal',
|
||||
locale: 'en',
|
||||
entries,
|
||||
});
|
||||
expect(words.title).toEqual({ text: 'Sintra', binding: { source: 'entry.title', entryId: 4 } });
|
||||
expect(words.subtitle).toEqual({ text: 'Serra de Sintra', binding: { source: 'entry.location', entryId: 4 } });
|
||||
expect(words.date?.binding).toEqual({ source: 'entry.date', entryId: 4, value: '2026-05-14' });
|
||||
expect(words.date?.text).toMatch(/2026/);
|
||||
expect(words.body).toEqual({ text: 'Fog, then sun.', binding: { source: 'entry.story', entryId: 4 } });
|
||||
const placeOnly = bookTemplateWords(page({ role: 'inner', entryId: 5 }), {
|
||||
bookTitle: 'Portugal',
|
||||
locale: 'en',
|
||||
entries: [{ id: 5, title: null, location: 'Cascais', date: null, story: null }],
|
||||
});
|
||||
expect(placeOnly).toEqual({ title: { text: 'Cascais', binding: { source: 'entry.location', entryId: 5 } } });
|
||||
expect(bookTemplateWords(page({ role: 'inner' }), { bookTitle: 'Portugal', locale: 'en', entries })).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
import { classifyBookTexts } from '../book-layouts';
|
||||
import { formatBookDate } from '../book-text';
|
||||
import { MAX_TEXT_LENGTH, type BookElement, type BookSpread, type BookTextElement } from '../book.schema';
|
||||
import { BOOK_TEMPLATE_TEXT_ROLES, type BookTemplateTextRole } from './template-format';
|
||||
|
||||
/**
|
||||
* Which of a page's words go into which of a template's text roles, and the
|
||||
* words a role takes when the page has none for it.
|
||||
*
|
||||
* ── The page's own words first ──────────────────────────────────────────
|
||||
*
|
||||
* A text bound to the journal says what it is: an entry's or the journey's
|
||||
* title is a title, a place a subtitle, a date a date, a story the body, a
|
||||
* photo's caption a caption. What is left is read the way the built-in
|
||||
* layouts read a page (`classifyBookTexts`): the largest type is the title,
|
||||
* the longest of the rest the story, the next a date line. A long text only
|
||||
* ever goes into the body, so a story is never set as a poster title.
|
||||
*
|
||||
* ── Then the book's ─────────────────────────────────────────────────────
|
||||
*
|
||||
* A role the page has no words for takes them from the book: a cover the
|
||||
* book's title and the year it was travelled, a page made from a journal
|
||||
* entry that entry's title, place, date and story, bound to it so the page
|
||||
* follows the journal. A role with no words at all is left off the page.
|
||||
*/
|
||||
|
||||
type TextEl = BookTextElement;
|
||||
type Binding = TextEl['binding'];
|
||||
|
||||
/** Words for a role: what they say, and the journal field they stay in step with. */
|
||||
export interface BookTemplateWord {
|
||||
text: string;
|
||||
binding?: Binding;
|
||||
}
|
||||
export type BookTemplateWords = Partial<Record<BookTemplateTextRole, BookTemplateWord>>;
|
||||
|
||||
/** Past this many characters a text is running text, which only the body takes. */
|
||||
const SHORT = 120;
|
||||
|
||||
const ROLE_OF_SOURCE: Record<NonNullable<Binding>['source'], BookTemplateTextRole> = {
|
||||
'journey.title': 'title',
|
||||
'entry.title': 'title',
|
||||
'journey.subtitle': 'subtitle',
|
||||
'entry.location': 'subtitle',
|
||||
'entry.date': 'date',
|
||||
'entry.story': 'body',
|
||||
'photo.caption': 'caption',
|
||||
};
|
||||
|
||||
/** The role a text bound to the journal reads as, by the field it is bound to; undefined for an unbound one. */
|
||||
export function templateRoleOfBinding(binding: Binding | undefined): BookTemplateTextRole | undefined {
|
||||
return binding ? ROLE_OF_SOURCE[binding.source] : undefined;
|
||||
}
|
||||
|
||||
/** Where a short text goes when the role it reads as is taken. */
|
||||
const SPARE_ORDER: readonly BookTemplateTextRole[] = ['subtitle', 'date', 'caption', 'title', 'body'];
|
||||
|
||||
/** A year, or a day and a month in figures: a line that reads as a date. */
|
||||
const DATE_LIKE = /\b(1[89]|20)\d{2}\b|\b\d{1,2}[./]\d{1,2}\b/;
|
||||
|
||||
const isText = (e: BookElement | null | undefined): e is TextEl => !!e && e.kind === 'text' && !!e.text.trim();
|
||||
const isLong = (t: TextEl) => t.text.trim().length > SHORT;
|
||||
|
||||
/** The role a short text reads as on its own: a date line when it says a date, else a subtitle. */
|
||||
const readsAs = (t: TextEl): BookTemplateTextRole => (DATE_LIKE.test(t.text) ? 'date' : 'subtitle');
|
||||
|
||||
/**
|
||||
* The page's texts by the role each takes, of the roles the template has.
|
||||
* Texts no role takes are left out, for the caller to park.
|
||||
*/
|
||||
export function assignTemplateTexts(
|
||||
texts: readonly BookElement[],
|
||||
roles: ReadonlySet<BookTemplateTextRole>,
|
||||
): Map<BookTemplateTextRole, TextEl> {
|
||||
const out = new Map<BookTemplateTextRole, TextEl>();
|
||||
const used = new Set<TextEl>();
|
||||
const free = (role: BookTemplateTextRole) => roles.has(role) && !out.has(role);
|
||||
const put = (role: BookTemplateTextRole, t: TextEl) => {
|
||||
out.set(role, t);
|
||||
used.add(t);
|
||||
};
|
||||
const pool = texts.filter(isText);
|
||||
|
||||
for (const t of pool) {
|
||||
const role = templateRoleOfBinding(t.binding);
|
||||
if (role && free(role)) put(role, t);
|
||||
}
|
||||
const rest = pool.filter((t) => !used.has(t));
|
||||
const place = (t: TextEl, first: BookTemplateTextRole) => {
|
||||
if (used.has(t)) return;
|
||||
const role = (isLong(t) ? (['body'] as const) : [first, ...SPARE_ORDER]).find(free);
|
||||
if (role) put(role, t);
|
||||
};
|
||||
const { heading, body, meta } = classifyBookTexts(rest);
|
||||
// Without type set as a heading, the first short line is the title, while the title is still free.
|
||||
const title = isText(heading) ? heading : free('title') ? rest.find((t) => !isLong(t)) : undefined;
|
||||
if (title) place(title, 'title');
|
||||
for (const t of [body, meta, ...rest]) if (isText(t)) place(t, isLong(t) ? 'body' : readsAs(t));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The parts of a journey a page's words come from, as the Studio and the book engine both know them. */
|
||||
export interface BookTemplateWordSource {
|
||||
/** The book's own title, which a cover is set in. */
|
||||
bookTitle: string;
|
||||
/** The journey's subtitle, when it has one. */
|
||||
subtitle?: string | null;
|
||||
locale: string;
|
||||
entries?: readonly {
|
||||
id: number;
|
||||
title?: string | null;
|
||||
story?: string | null;
|
||||
location?: string | null;
|
||||
date?: string | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
const clip = (s: string) => s.trim().slice(0, MAX_TEXT_LENGTH);
|
||||
|
||||
/** A cover's words: the book's title, the journey's subtitle and the year of its first dated stop. */
|
||||
function singlePageWords(src: BookTemplateWordSource): BookTemplateWords {
|
||||
const out: BookTemplateWords = {};
|
||||
if (src.bookTitle.trim()) out.title = { text: clip(src.bookTitle) };
|
||||
if (src.subtitle?.trim()) out.subtitle = { text: clip(src.subtitle), binding: { source: 'journey.subtitle' } };
|
||||
const dated = src.entries?.find((e) => e.date && /^\d{4}/.test(e.date))?.date;
|
||||
if (dated) out.date = { text: dated.slice(0, 4) };
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A page's words from the journal entry it was made from, each bound to the field it came from. */
|
||||
function entryWords(entry: NonNullable<BookTemplateWordSource['entries']>[number], locale: string): BookTemplateWords {
|
||||
const out: BookTemplateWords = {};
|
||||
const title = entry.title?.trim() || entry.location?.trim();
|
||||
if (title) {
|
||||
const source = entry.title?.trim() ? 'entry.title' : 'entry.location';
|
||||
out.title = { text: clip(title), binding: { source, entryId: entry.id } };
|
||||
}
|
||||
if (entry.title?.trim() && entry.location?.trim()) {
|
||||
out.subtitle = { text: clip(entry.location), binding: { source: 'entry.location', entryId: entry.id } };
|
||||
}
|
||||
const date = entry.date ? formatBookDate(entry.date, locale) : '';
|
||||
if (date && entry.date) {
|
||||
out.date = { text: date, binding: { source: 'entry.date', entryId: entry.id, value: entry.date } };
|
||||
}
|
||||
if (entry.story?.trim())
|
||||
out.body = { text: clip(entry.story), binding: { source: 'entry.story', entryId: entry.id } };
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The words a template's empty roles take on this page. */
|
||||
export function bookTemplateWords(spread: BookSpread, src: BookTemplateWordSource): BookTemplateWords {
|
||||
if (spread.role !== 'inner') return singlePageWords(src);
|
||||
const entry = spread.entryId === null ? undefined : src.entries?.find((e) => e.id === spread.entryId);
|
||||
return entry ? entryWords(entry, src.locale) : {};
|
||||
}
|
||||
|
||||
/** The text roles a template element can carry, as a set for `assignTemplateTexts`. */
|
||||
export function textRolesOf(roles: readonly string[]): Set<BookTemplateTextRole> {
|
||||
return new Set(
|
||||
roles.filter((r): r is BookTemplateTextRole => (BOOK_TEMPLATE_TEXT_ROLES as readonly string[]).includes(r)),
|
||||
);
|
||||
}
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'صفحات مصممة',
|
||||
'studio.templates.style': 'النمط',
|
||||
'studio.templates.all': 'الكل',
|
||||
'studio.templates.hint':
|
||||
'يُعرض كل تصميم بصور هذه الصفحة ونصوصها. بنقرة واحدة تُنسَّق الصفحة وفقه، وما لا يتسع له يبقى مع الصفحة.',
|
||||
'studio.templates.archetype.spine-band': 'شريط الكعب',
|
||||
'studio.templates.archetype.poster-pop': 'ملصق',
|
||||
'studio.templates.archetype.cutout': 'قصاصات',
|
||||
'studio.templates.archetype.postcard': 'بطاقة بريدية',
|
||||
'studio.templates.archetype.grid-badge': 'شبكة وشارة',
|
||||
'studio.templates.archetype.minimal-serif': 'بسيط',
|
||||
'studio.templates.archetype.stamp-collage': 'طوابع',
|
||||
'studio.templates.name.spineBandStitched': 'شريط مخيط',
|
||||
'studio.templates.name.spineBandGingham': 'شريط مربعات',
|
||||
'studio.templates.name.spineBandScript': 'شريط وخط يدوي',
|
||||
'studio.templates.name.posterPopSunburst': 'ملصق بأشعة',
|
||||
'studio.templates.name.posterPopSplit': 'ملصق مقسوم',
|
||||
'studio.templates.name.posterPopStack': 'ملصق متراكب',
|
||||
'studio.templates.name.cutoutMatisse': 'قصاصات ورق',
|
||||
'studio.templates.name.cutoutArch': 'قوس مقصوص',
|
||||
'studio.templates.name.cutoutCollage': 'كولاج قصاصات',
|
||||
'studio.templates.name.postcardHouses': 'بطاقة المدينة',
|
||||
'studio.templates.name.postcardCoast': 'بطاقة الساحل',
|
||||
'studio.templates.name.postcardPeaks': 'بطاقة الجبال',
|
||||
'studio.templates.name.gridBadgeCircle': 'شبكة بدائرة',
|
||||
'studio.templates.name.gridBadgeStamp': 'شبكة بختم',
|
||||
'studio.templates.name.gridBadgeSeal': 'شبكة بشارة نجمية',
|
||||
'studio.templates.name.minimalSerifClassic': 'خط مذيّل كلاسيكي',
|
||||
'studio.templates.name.minimalSerifPortrait': 'خط مذيّل عمودي',
|
||||
'studio.templates.name.minimalSerifWindow': 'خط مذيّل بنافذة',
|
||||
'studio.templates.name.stampCollageChecker': 'كولاج رقعة الشطرنج',
|
||||
'studio.templates.name.stampCollageRow': 'صف طوابع',
|
||||
'studio.templates.name.stampCollageArches': 'أقواس على رقعة شطرنج',
|
||||
'studio.templates.name.heroScriptWord': 'صورة وخط يدوي',
|
||||
'studio.templates.name.heroTrioWordart': 'كلمة مصوّرة وثلاث صور',
|
||||
'studio.templates.name.storyQuote': 'قصة واقتباس',
|
||||
'studio.templates.name.bleedBandCaption': 'بانوراما بشريط',
|
||||
'studio.templates.name.twoToneSplit': 'لونان',
|
||||
'studio.templates.name.dayOpener': 'افتتاحية اليوم',
|
||||
'studio.templates.name.polaroidStack': 'صور بولارويد',
|
||||
'studio.templates.name.stampRow': 'طوابع متجاورة',
|
||||
'studio.templates.name.archGallery': 'معرض الأقواس',
|
||||
'studio.templates.name.mapTickets': 'خريطة وتذاكر',
|
||||
'studio.templates.name.filmstripScript': 'شريط فيلم',
|
||||
'studio.templates.name.postcardBack': 'ظهر بطاقة بريدية',
|
||||
'studio.templates.name.gridTight': 'شبكة متقاربة',
|
||||
'studio.templates.name.gridFeature': 'شبكة بصورة رئيسية',
|
||||
'studio.templates.name.gridColumns': 'شبكة أعمدة',
|
||||
'studio.templates.name.gridMosaic': 'شبكة فسيفساء',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Dizayn olunmuş səhifələr',
|
||||
'studio.templates.style': 'Üslub',
|
||||
'studio.templates.all': 'Hamısı',
|
||||
'studio.templates.hint':
|
||||
'Hər dizayn bu səhifənin fotoşəkilləri və mətnləri ilə göstərilir. Bir klik səhifəni ona görə düzür, yer tapmayanlar isə səhifədə qalır.',
|
||||
'studio.templates.archetype.spine-band': 'Cild zolağı',
|
||||
'studio.templates.archetype.poster-pop': 'Afişa',
|
||||
'studio.templates.archetype.cutout': 'Kəsmə kağız',
|
||||
'studio.templates.archetype.postcard': 'Açıqca',
|
||||
'studio.templates.archetype.grid-badge': 'Şəbəkə və nişan',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimal', // same-as-en
|
||||
'studio.templates.archetype.stamp-collage': 'Markalar',
|
||||
'studio.templates.name.spineBandStitched': 'Tikişli zolaq',
|
||||
'studio.templates.name.spineBandGingham': 'Damalı zolaq',
|
||||
'studio.templates.name.spineBandScript': 'Zolaq və əl yazısı',
|
||||
'studio.templates.name.posterPopSunburst': 'Şüalı afişa',
|
||||
'studio.templates.name.posterPopSplit': 'Bölünmüş afişa',
|
||||
'studio.templates.name.posterPopStack': 'Laylı afişa',
|
||||
'studio.templates.name.cutoutMatisse': 'Kağız kəsikləri',
|
||||
'studio.templates.name.cutoutArch': 'Kəsilmiş tağ',
|
||||
'studio.templates.name.cutoutCollage': 'Kəsmə kollaj',
|
||||
'studio.templates.name.postcardHouses': 'Şəhərli açıqca',
|
||||
'studio.templates.name.postcardCoast': 'Sahilli açıqca',
|
||||
'studio.templates.name.postcardPeaks': 'Dağlı açıqca',
|
||||
'studio.templates.name.gridBadgeCircle': 'Dairəli şəbəkə',
|
||||
'studio.templates.name.gridBadgeStamp': 'Ştamplı şəbəkə',
|
||||
'studio.templates.name.gridBadgeSeal': 'Möhürlü şəbəkə',
|
||||
'studio.templates.name.minimalSerifClassic': 'Klassik antikva',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Şaquli antikva',
|
||||
'studio.templates.name.minimalSerifWindow': 'Pəncərəli antikva',
|
||||
'studio.templates.name.stampCollageChecker': 'Şahmat kollajı',
|
||||
'studio.templates.name.stampCollageRow': 'Marka sırası',
|
||||
'studio.templates.name.stampCollageArches': 'Şahmat üzərində tağlar',
|
||||
'studio.templates.name.heroScriptWord': 'Foto və əl yazısı',
|
||||
'studio.templates.name.heroTrioWordart': 'Fotolu söz və üç foto',
|
||||
'studio.templates.name.storyQuote': 'Hekayə və sitat',
|
||||
'studio.templates.name.bleedBandCaption': 'Zolaqlı panorama',
|
||||
'studio.templates.name.twoToneSplit': 'İki ton',
|
||||
'studio.templates.name.dayOpener': 'Günün açılışı',
|
||||
'studio.templates.name.polaroidStack': 'Polaroidlər',
|
||||
'studio.templates.name.stampRow': 'Yan-yana markalar',
|
||||
'studio.templates.name.archGallery': 'Tağ qalereyası',
|
||||
'studio.templates.name.mapTickets': 'Xəritə və biletlər',
|
||||
'studio.templates.name.filmstripScript': 'Film lenti',
|
||||
'studio.templates.name.postcardBack': 'Açıqcanın arxası',
|
||||
'studio.templates.name.gridTight': 'Sıx şəbəkə',
|
||||
'studio.templates.name.gridFeature': 'Əsas fotolu şəbəkə',
|
||||
'studio.templates.name.gridColumns': 'Sütunlu şəbəkə',
|
||||
'studio.templates.name.gridMosaic': 'Mozaika şəbəkəsi',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Páginas desenhadas',
|
||||
'studio.templates.style': 'Estilo',
|
||||
'studio.templates.all': 'Todos',
|
||||
'studio.templates.hint':
|
||||
'Cada modelo é desenhado com as fotos e os textos desta página. Um clique diagrama a página com ele, e o que não couber fica com a página.',
|
||||
'studio.templates.archetype.spine-band': 'Faixa na lombada',
|
||||
'studio.templates.archetype.poster-pop': 'Pôster',
|
||||
'studio.templates.archetype.cutout': 'Recortes',
|
||||
'studio.templates.archetype.postcard': 'Cartão-postal',
|
||||
'studio.templates.archetype.grid-badge': 'Grade e selo',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimalista',
|
||||
'studio.templates.archetype.stamp-collage': 'Selos',
|
||||
'studio.templates.name.spineBandStitched': 'Faixa costurada',
|
||||
'studio.templates.name.spineBandGingham': 'Faixa xadrez',
|
||||
'studio.templates.name.spineBandScript': 'Faixa e caligrafia',
|
||||
'studio.templates.name.posterPopSunburst': 'Pôster com raios',
|
||||
'studio.templates.name.posterPopSplit': 'Pôster dividido',
|
||||
'studio.templates.name.posterPopStack': 'Pôster empilhado',
|
||||
'studio.templates.name.cutoutMatisse': 'Papéis recortados',
|
||||
'studio.templates.name.cutoutArch': 'Arco recortado',
|
||||
'studio.templates.name.cutoutCollage': 'Colagem recortada',
|
||||
'studio.templates.name.postcardHouses': 'Postal da cidade',
|
||||
'studio.templates.name.postcardCoast': 'Postal do litoral',
|
||||
'studio.templates.name.postcardPeaks': 'Postal da montanha',
|
||||
'studio.templates.name.gridBadgeCircle': 'Grade com círculo',
|
||||
'studio.templates.name.gridBadgeStamp': 'Grade com carimbo',
|
||||
'studio.templates.name.gridBadgeSeal': 'Grade com selo',
|
||||
'studio.templates.name.minimalSerifClassic': 'Serifa clássica',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Serifa vertical',
|
||||
'studio.templates.name.minimalSerifWindow': 'Serifa com janela',
|
||||
'studio.templates.name.stampCollageChecker': 'Colagem xadrez',
|
||||
'studio.templates.name.stampCollageRow': 'Fileira de selos',
|
||||
'studio.templates.name.stampCollageArches': 'Arcos no xadrez',
|
||||
'studio.templates.name.heroScriptWord': 'Foto e caligrafia',
|
||||
'studio.templates.name.heroTrioWordart': 'Palavra-imagem e três fotos',
|
||||
'studio.templates.name.storyQuote': 'História e citação',
|
||||
'studio.templates.name.bleedBandCaption': 'Panorama com faixa',
|
||||
'studio.templates.name.twoToneSplit': 'Dois tons',
|
||||
'studio.templates.name.dayOpener': 'Abertura do dia',
|
||||
'studio.templates.name.polaroidStack': 'Polaroides',
|
||||
'studio.templates.name.stampRow': 'Selos lado a lado',
|
||||
'studio.templates.name.archGallery': 'Galeria de arcos',
|
||||
'studio.templates.name.mapTickets': 'Mapa e passagens',
|
||||
'studio.templates.name.filmstripScript': 'Filme fotográfico',
|
||||
'studio.templates.name.postcardBack': 'Verso de cartão-postal',
|
||||
'studio.templates.name.gridTight': 'Grade justa',
|
||||
'studio.templates.name.gridFeature': 'Grade em destaque',
|
||||
'studio.templates.name.gridColumns': 'Grade em colunas',
|
||||
'studio.templates.name.gridMosaic': 'Grade mosaico',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Pàgines dissenyades',
|
||||
'studio.templates.style': 'Estil',
|
||||
'studio.templates.all': 'Totes',
|
||||
'studio.templates.hint':
|
||||
'Cada disseny es mostra amb les fotos i els textos d’aquesta pàgina. Un clic hi compon la pàgina, i el que no hi cap es queda amb la pàgina.',
|
||||
'studio.templates.archetype.spine-band': 'Banda al llom',
|
||||
'studio.templates.archetype.poster-pop': 'Pòster',
|
||||
'studio.templates.archetype.cutout': 'Retalls',
|
||||
'studio.templates.archetype.postcard': 'Postal',
|
||||
'studio.templates.archetype.grid-badge': 'Quadrícula i insígnia',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimalista',
|
||||
'studio.templates.archetype.stamp-collage': 'Segells',
|
||||
'studio.templates.name.spineBandStitched': 'Banda cosida',
|
||||
'studio.templates.name.spineBandGingham': 'Banda de quadres',
|
||||
'studio.templates.name.spineBandScript': 'Banda i cal·ligrafia',
|
||||
'studio.templates.name.posterPopSunburst': 'Pòster de raigs',
|
||||
'studio.templates.name.posterPopSplit': 'Pòster partit',
|
||||
'studio.templates.name.posterPopStack': 'Pòster apilat',
|
||||
'studio.templates.name.cutoutMatisse': 'Papers retallats',
|
||||
'studio.templates.name.cutoutArch': 'Arc retallat',
|
||||
'studio.templates.name.cutoutCollage': 'Collage de retalls',
|
||||
'studio.templates.name.postcardHouses': 'Postal de ciutat',
|
||||
'studio.templates.name.postcardCoast': 'Postal de costa',
|
||||
'studio.templates.name.postcardPeaks': 'Postal de muntanya',
|
||||
'studio.templates.name.gridBadgeCircle': 'Quadrícula amb cercle',
|
||||
'studio.templates.name.gridBadgeStamp': 'Quadrícula amb segell',
|
||||
'studio.templates.name.gridBadgeSeal': 'Quadrícula amb medalla',
|
||||
'studio.templates.name.minimalSerifClassic': 'Serifa clàssica',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Serifa vertical',
|
||||
'studio.templates.name.minimalSerifWindow': 'Serifa amb finestra',
|
||||
'studio.templates.name.stampCollageChecker': 'Collage d’escacs',
|
||||
'studio.templates.name.stampCollageRow': 'Filera de segells',
|
||||
'studio.templates.name.stampCollageArches': 'Arcs sobre escacs',
|
||||
'studio.templates.name.heroScriptWord': 'Foto i cal·ligrafia',
|
||||
'studio.templates.name.heroTrioWordart': 'Paraula imatge i tres fotos',
|
||||
'studio.templates.name.storyQuote': 'Relat i cita',
|
||||
'studio.templates.name.bleedBandCaption': 'Panoràmica amb banda',
|
||||
'studio.templates.name.twoToneSplit': 'Dos tons',
|
||||
'studio.templates.name.dayOpener': 'Obertura del dia',
|
||||
'studio.templates.name.polaroidStack': 'Polaroids', // same-as-en
|
||||
'studio.templates.name.stampRow': 'Segells en fila',
|
||||
'studio.templates.name.archGallery': 'Galeria d’arcs',
|
||||
'studio.templates.name.mapTickets': 'Mapa i bitllets',
|
||||
'studio.templates.name.filmstripScript': 'Tira de pel·lícula',
|
||||
'studio.templates.name.postcardBack': 'Revers de postal',
|
||||
'studio.templates.name.gridTight': 'Quadrícula estreta',
|
||||
'studio.templates.name.gridFeature': 'Quadrícula destacada',
|
||||
'studio.templates.name.gridColumns': 'Quadrícula en columnes',
|
||||
'studio.templates.name.gridMosaic': 'Quadrícula mosaic',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Navržené stránky',
|
||||
'studio.templates.style': 'Styl',
|
||||
'studio.templates.all': 'Vše',
|
||||
'studio.templates.hint':
|
||||
'Každý návrh je zobrazen s fotkami a texty této stránky. Kliknutím stránku podle něj rozvrhnete, a co se nevejde, zůstane u stránky.',
|
||||
'studio.templates.archetype.spine-band': 'Pruh na hřbetu',
|
||||
'studio.templates.archetype.poster-pop': 'Plakát',
|
||||
'studio.templates.archetype.cutout': 'Vystřihovánky',
|
||||
'studio.templates.archetype.postcard': 'Pohlednice',
|
||||
'studio.templates.archetype.grid-badge': 'Mřížka a odznak',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimalismus',
|
||||
'studio.templates.archetype.stamp-collage': 'Známky',
|
||||
'studio.templates.name.spineBandStitched': 'Prošitý pruh',
|
||||
'studio.templates.name.spineBandGingham': 'Kostkovaný pruh',
|
||||
'studio.templates.name.spineBandScript': 'Pruh s psacím písmem',
|
||||
'studio.templates.name.posterPopSunburst': 'Plakát s paprsky',
|
||||
'studio.templates.name.posterPopSplit': 'Rozdělený plakát',
|
||||
'studio.templates.name.posterPopStack': 'Vrstvený plakát',
|
||||
'studio.templates.name.cutoutMatisse': 'Papírové výstřižky',
|
||||
'studio.templates.name.cutoutArch': 'Vystřižený oblouk',
|
||||
'studio.templates.name.cutoutCollage': 'Koláž z výstřižků',
|
||||
'studio.templates.name.postcardHouses': 'Pohlednice s městem',
|
||||
'studio.templates.name.postcardCoast': 'Pohlednice s pobřežím',
|
||||
'studio.templates.name.postcardPeaks': 'Pohlednice s horami',
|
||||
'studio.templates.name.gridBadgeCircle': 'Mřížka s kruhem',
|
||||
'studio.templates.name.gridBadgeStamp': 'Mřížka s razítkem',
|
||||
'studio.templates.name.gridBadgeSeal': 'Mřížka s pečetí',
|
||||
'studio.templates.name.minimalSerifClassic': 'Klasická patková',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Patková na výšku',
|
||||
'studio.templates.name.minimalSerifWindow': 'Patková s oknem',
|
||||
'studio.templates.name.stampCollageChecker': 'Šachovnicová koláž',
|
||||
'studio.templates.name.stampCollageRow': 'Řada známek',
|
||||
'studio.templates.name.stampCollageArches': 'Oblouky na šachovnici',
|
||||
'studio.templates.name.heroScriptWord': 'Fotka a psací písmo',
|
||||
'studio.templates.name.heroTrioWordart': 'Slovo z fotky a tři fotky',
|
||||
'studio.templates.name.storyQuote': 'Příběh s citátem',
|
||||
'studio.templates.name.bleedBandCaption': 'Panorama s pruhem',
|
||||
'studio.templates.name.twoToneSplit': 'Dvě barvy',
|
||||
'studio.templates.name.dayOpener': 'Úvod dne',
|
||||
'studio.templates.name.polaroidStack': 'Polaroidy',
|
||||
'studio.templates.name.stampRow': 'Známky v řadě',
|
||||
'studio.templates.name.archGallery': 'Galerie oblouků',
|
||||
'studio.templates.name.mapTickets': 'Mapa a lístky',
|
||||
'studio.templates.name.filmstripScript': 'Filmový pás',
|
||||
'studio.templates.name.postcardBack': 'Rub pohlednice',
|
||||
'studio.templates.name.gridTight': 'Těsná mřížka',
|
||||
'studio.templates.name.gridFeature': 'Mřížka s hlavní fotkou',
|
||||
'studio.templates.name.gridColumns': 'Sloupcová mřížka',
|
||||
'studio.templates.name.gridMosaic': 'Mozaiková mřížka',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Gestaltete Seiten',
|
||||
'studio.templates.style': 'Stil',
|
||||
'studio.templates.all': 'Alle',
|
||||
'studio.templates.hint':
|
||||
'Jeder Entwurf zeigt die Fotos und Texte dieser Seite. Ein Klick gestaltet die Seite danach, und was keinen Platz findet, bleibt bei der Seite.',
|
||||
'studio.templates.archetype.spine-band': 'Buchrückenband',
|
||||
'studio.templates.archetype.poster-pop': 'Poster', // same-as-en
|
||||
'studio.templates.archetype.cutout': 'Scherenschnitt',
|
||||
'studio.templates.archetype.postcard': 'Postkarte',
|
||||
'studio.templates.archetype.grid-badge': 'Raster mit Plakette',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimal', // same-as-en
|
||||
'studio.templates.archetype.stamp-collage': 'Briefmarken',
|
||||
'studio.templates.name.spineBandStitched': 'Genähtes Band',
|
||||
'studio.templates.name.spineBandGingham': 'Band mit Vichykaro',
|
||||
'studio.templates.name.spineBandScript': 'Band mit Schreibschrift',
|
||||
'studio.templates.name.posterPopSunburst': 'Poster mit Strahlen',
|
||||
'studio.templates.name.posterPopSplit': 'Geteiltes Poster',
|
||||
'studio.templates.name.posterPopStack': 'Gestapeltes Poster',
|
||||
'studio.templates.name.cutoutMatisse': 'Papierschnitte',
|
||||
'studio.templates.name.cutoutArch': 'Bogen im Scherenschnitt',
|
||||
'studio.templates.name.cutoutCollage': 'Scherenschnitt-Collage',
|
||||
'studio.templates.name.postcardHouses': 'Postkarte mit Stadt',
|
||||
'studio.templates.name.postcardCoast': 'Postkarte mit Küste',
|
||||
'studio.templates.name.postcardPeaks': 'Postkarte mit Bergen',
|
||||
'studio.templates.name.gridBadgeCircle': 'Raster mit Kreis',
|
||||
'studio.templates.name.gridBadgeStamp': 'Raster mit Stempel',
|
||||
'studio.templates.name.gridBadgeSeal': 'Raster mit Siegel',
|
||||
'studio.templates.name.minimalSerifClassic': 'Klassische Serife',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Serife im Hochformat',
|
||||
'studio.templates.name.minimalSerifWindow': 'Serife mit Fenster',
|
||||
'studio.templates.name.stampCollageChecker': 'Karo-Collage',
|
||||
'studio.templates.name.stampCollageRow': 'Briefmarkenreihe',
|
||||
'studio.templates.name.stampCollageArches': 'Bögen auf Karo',
|
||||
'studio.templates.name.heroScriptWord': 'Titelbild mit Schreibschrift',
|
||||
'studio.templates.name.heroTrioWordart': 'Wortbild und drei Fotos',
|
||||
'studio.templates.name.storyQuote': 'Geschichte mit Zitat',
|
||||
'studio.templates.name.bleedBandCaption': 'Panorama mit Band',
|
||||
'studio.templates.name.twoToneSplit': 'Zwei Farben',
|
||||
'studio.templates.name.dayOpener': 'Tagesauftakt',
|
||||
'studio.templates.name.polaroidStack': 'Polaroids', // same-as-en
|
||||
'studio.templates.name.stampRow': 'Briefmarken quer',
|
||||
'studio.templates.name.archGallery': 'Bogengalerie',
|
||||
'studio.templates.name.mapTickets': 'Karte und Tickets',
|
||||
'studio.templates.name.filmstripScript': 'Filmstreifen',
|
||||
'studio.templates.name.postcardBack': 'Postkartenrückseite',
|
||||
'studio.templates.name.gridTight': 'Enges Raster',
|
||||
'studio.templates.name.gridFeature': 'Raster mit Hauptbild',
|
||||
'studio.templates.name.gridColumns': 'Spaltenraster',
|
||||
'studio.templates.name.gridMosaic': 'Mosaikraster',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Designed pages',
|
||||
'studio.templates.style': 'Style',
|
||||
'studio.templates.all': 'All',
|
||||
'studio.templates.hint':
|
||||
"Each design is drawn with this page's photos and words. A click lays the page out on it, and what it has no room for stays with the page.",
|
||||
'studio.templates.archetype.spine-band': 'Spine band',
|
||||
'studio.templates.archetype.poster-pop': 'Poster',
|
||||
'studio.templates.archetype.cutout': 'Cut-out',
|
||||
'studio.templates.archetype.postcard': 'Postcard',
|
||||
'studio.templates.archetype.grid-badge': 'Grid and badge',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimal',
|
||||
'studio.templates.archetype.stamp-collage': 'Stamps',
|
||||
'studio.templates.name.spineBandStitched': 'Stitched band',
|
||||
'studio.templates.name.spineBandGingham': 'Gingham band',
|
||||
'studio.templates.name.spineBandScript': 'Band with script',
|
||||
'studio.templates.name.posterPopSunburst': 'Sunburst poster',
|
||||
'studio.templates.name.posterPopSplit': 'Split poster',
|
||||
'studio.templates.name.posterPopStack': 'Stacked poster',
|
||||
'studio.templates.name.cutoutMatisse': 'Paper cut-outs',
|
||||
'studio.templates.name.cutoutArch': 'Cut-out arch',
|
||||
'studio.templates.name.cutoutCollage': 'Cut-out collage',
|
||||
'studio.templates.name.postcardHouses': 'Town postcard',
|
||||
'studio.templates.name.postcardCoast': 'Coast postcard',
|
||||
'studio.templates.name.postcardPeaks': 'Mountain postcard',
|
||||
'studio.templates.name.gridBadgeCircle': 'Grid with circle',
|
||||
'studio.templates.name.gridBadgeStamp': 'Grid with stamp',
|
||||
'studio.templates.name.gridBadgeSeal': 'Grid with seal',
|
||||
'studio.templates.name.minimalSerifClassic': 'Classic serif',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Portrait serif',
|
||||
'studio.templates.name.minimalSerifWindow': 'Window serif',
|
||||
'studio.templates.name.stampCollageChecker': 'Checker collage',
|
||||
'studio.templates.name.stampCollageRow': 'Stamp row',
|
||||
'studio.templates.name.stampCollageArches': 'Arches on checker',
|
||||
'studio.templates.name.heroScriptWord': 'Hero and script',
|
||||
'studio.templates.name.heroTrioWordart': 'Word art and three',
|
||||
'studio.templates.name.storyQuote': 'Story with quote',
|
||||
'studio.templates.name.bleedBandCaption': 'Panorama with band',
|
||||
'studio.templates.name.twoToneSplit': 'Two tones',
|
||||
'studio.templates.name.dayOpener': 'Day opener',
|
||||
'studio.templates.name.polaroidStack': 'Polaroids',
|
||||
'studio.templates.name.stampRow': 'Stamps across',
|
||||
'studio.templates.name.archGallery': 'Arch gallery',
|
||||
'studio.templates.name.mapTickets': 'Map and tickets',
|
||||
'studio.templates.name.filmstripScript': 'Filmstrip',
|
||||
'studio.templates.name.postcardBack': 'Postcard back',
|
||||
'studio.templates.name.gridTight': 'Tight grid',
|
||||
'studio.templates.name.gridFeature': 'Feature grid',
|
||||
'studio.templates.name.gridColumns': 'Column grid',
|
||||
'studio.templates.name.gridMosaic': 'Mosaic grid',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Páginas diseñadas',
|
||||
'studio.templates.style': 'Estilo',
|
||||
'studio.templates.all': 'Todas',
|
||||
'studio.templates.hint':
|
||||
'Cada diseño se muestra con las fotos y los textos de esta página. Un clic maqueta la página con él, y lo que no cabe se queda con la página.',
|
||||
'studio.templates.archetype.spine-band': 'Banda de lomo',
|
||||
'studio.templates.archetype.poster-pop': 'Póster',
|
||||
'studio.templates.archetype.cutout': 'Recortes',
|
||||
'studio.templates.archetype.postcard': 'Postal',
|
||||
'studio.templates.archetype.grid-badge': 'Cuadrícula e insignia',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimalista',
|
||||
'studio.templates.archetype.stamp-collage': 'Sellos',
|
||||
'studio.templates.name.spineBandStitched': 'Banda cosida',
|
||||
'studio.templates.name.spineBandGingham': 'Banda de vichy',
|
||||
'studio.templates.name.spineBandScript': 'Banda y caligrafía',
|
||||
'studio.templates.name.posterPopSunburst': 'Póster de rayos',
|
||||
'studio.templates.name.posterPopSplit': 'Póster dividido',
|
||||
'studio.templates.name.posterPopStack': 'Póster apilado',
|
||||
'studio.templates.name.cutoutMatisse': 'Papeles recortados',
|
||||
'studio.templates.name.cutoutArch': 'Arco recortado',
|
||||
'studio.templates.name.cutoutCollage': 'Collage recortado',
|
||||
'studio.templates.name.postcardHouses': 'Postal de ciudad',
|
||||
'studio.templates.name.postcardCoast': 'Postal de costa',
|
||||
'studio.templates.name.postcardPeaks': 'Postal de montaña',
|
||||
'studio.templates.name.gridBadgeCircle': 'Cuadrícula con círculo',
|
||||
'studio.templates.name.gridBadgeStamp': 'Cuadrícula con sello',
|
||||
'studio.templates.name.gridBadgeSeal': 'Cuadrícula con medalla',
|
||||
'studio.templates.name.minimalSerifClassic': 'Serifa clásica',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Serifa vertical',
|
||||
'studio.templates.name.minimalSerifWindow': 'Serifa con ventana',
|
||||
'studio.templates.name.stampCollageChecker': 'Collage de damero',
|
||||
'studio.templates.name.stampCollageRow': 'Fila de sellos',
|
||||
'studio.templates.name.stampCollageArches': 'Arcos sobre damero',
|
||||
'studio.templates.name.heroScriptWord': 'Foto y caligrafía',
|
||||
'studio.templates.name.heroTrioWordart': 'Palabra imagen y tres fotos',
|
||||
'studio.templates.name.storyQuote': 'Relato y cita',
|
||||
'studio.templates.name.bleedBandCaption': 'Panorámica con banda',
|
||||
'studio.templates.name.twoToneSplit': 'Dos tonos',
|
||||
'studio.templates.name.dayOpener': 'Apertura del día',
|
||||
'studio.templates.name.polaroidStack': 'Polaroids', // same-as-en
|
||||
'studio.templates.name.stampRow': 'Sellos a lo ancho',
|
||||
'studio.templates.name.archGallery': 'Galería de arcos',
|
||||
'studio.templates.name.mapTickets': 'Mapa y billetes',
|
||||
'studio.templates.name.filmstripScript': 'Tira de película',
|
||||
'studio.templates.name.postcardBack': 'Reverso de postal',
|
||||
'studio.templates.name.gridTight': 'Cuadrícula estrecha',
|
||||
'studio.templates.name.gridFeature': 'Cuadrícula destacada',
|
||||
'studio.templates.name.gridColumns': 'Cuadrícula en columnas',
|
||||
'studio.templates.name.gridMosaic': 'Cuadrícula mosaico',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Kujundatud lehed',
|
||||
'studio.templates.style': 'Stiil',
|
||||
'studio.templates.all': 'Kõik',
|
||||
'studio.templates.hint':
|
||||
'Iga kujundus on joonistatud selle lehe fotode ja tekstidega. Klõps paigutab lehe selle järgi, ja mis ei mahu, jääb lehe juurde.',
|
||||
'studio.templates.archetype.spine-band': 'Seljariba',
|
||||
'studio.templates.archetype.poster-pop': 'Plakat',
|
||||
'studio.templates.archetype.cutout': 'Paberlõiked',
|
||||
'studio.templates.archetype.postcard': 'Postkaart',
|
||||
'studio.templates.archetype.grid-badge': 'Võrk ja märk',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimaalne',
|
||||
'studio.templates.archetype.stamp-collage': 'Margid',
|
||||
'studio.templates.name.spineBandStitched': 'Õmmeldud riba',
|
||||
'studio.templates.name.spineBandGingham': 'Ruuduline riba',
|
||||
'studio.templates.name.spineBandScript': 'Riba ja käekiri',
|
||||
'studio.templates.name.posterPopSunburst': 'Kiirtega plakat',
|
||||
'studio.templates.name.posterPopSplit': 'Jagatud plakat',
|
||||
'studio.templates.name.posterPopStack': 'Kihiline plakat',
|
||||
'studio.templates.name.cutoutMatisse': 'Lõigatud paberid',
|
||||
'studio.templates.name.cutoutArch': 'Lõigatud kaar',
|
||||
'studio.templates.name.cutoutCollage': 'Lõikekollaaž',
|
||||
'studio.templates.name.postcardHouses': 'Linnaga postkaart',
|
||||
'studio.templates.name.postcardCoast': 'Rannaga postkaart',
|
||||
'studio.templates.name.postcardPeaks': 'Mägedega postkaart',
|
||||
'studio.templates.name.gridBadgeCircle': 'Võrk ringiga',
|
||||
'studio.templates.name.gridBadgeStamp': 'Võrk templiga',
|
||||
'studio.templates.name.gridBadgeSeal': 'Võrk pitseriga',
|
||||
'studio.templates.name.minimalSerifClassic': 'Klassikaline antiikva',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Püstine antiikva',
|
||||
'studio.templates.name.minimalSerifWindow': 'Aknaga antiikva',
|
||||
'studio.templates.name.stampCollageChecker': 'Malelaua kollaaž',
|
||||
'studio.templates.name.stampCollageRow': 'Margirida',
|
||||
'studio.templates.name.stampCollageArches': 'Kaared malelaual',
|
||||
'studio.templates.name.heroScriptWord': 'Foto ja käekiri',
|
||||
'studio.templates.name.heroTrioWordart': 'Fotosõna ja kolm fotot',
|
||||
'studio.templates.name.storyQuote': 'Lugu ja tsitaat',
|
||||
'studio.templates.name.bleedBandCaption': 'Panoraam ribaga',
|
||||
'studio.templates.name.twoToneSplit': 'Kaks tooni',
|
||||
'studio.templates.name.dayOpener': 'Päeva avaleht',
|
||||
'studio.templates.name.polaroidStack': 'Polaroidid',
|
||||
'studio.templates.name.stampRow': 'Margid reas',
|
||||
'studio.templates.name.archGallery': 'Kaarte galerii',
|
||||
'studio.templates.name.mapTickets': 'Kaart ja piletid',
|
||||
'studio.templates.name.filmstripScript': 'Filmilint',
|
||||
'studio.templates.name.postcardBack': 'Postkaardi tagakülg',
|
||||
'studio.templates.name.gridTight': 'Tihe võrk',
|
||||
'studio.templates.name.gridFeature': 'Esiletõstetud võrk',
|
||||
'studio.templates.name.gridColumns': 'Veergudega võrk',
|
||||
'studio.templates.name.gridMosaic': 'Mosaiikvõrk',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Pages composées',
|
||||
'studio.templates.style': 'Style', // same-as-en
|
||||
'studio.templates.all': 'Tout',
|
||||
'studio.templates.hint':
|
||||
'Chaque modèle est dessiné avec les photos et les textes de cette page. Un clic met la page en forme selon ce modèle, et ce qui n’y trouve pas de place reste avec la page.',
|
||||
'studio.templates.archetype.spine-band': 'Bande de dos',
|
||||
'studio.templates.archetype.poster-pop': 'Affiche',
|
||||
'studio.templates.archetype.cutout': 'Papier découpé',
|
||||
'studio.templates.archetype.postcard': 'Carte postale',
|
||||
'studio.templates.archetype.grid-badge': 'Grille et badge',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimal', // same-as-en
|
||||
'studio.templates.archetype.stamp-collage': 'Timbres',
|
||||
'studio.templates.name.spineBandStitched': 'Bande cousue',
|
||||
'studio.templates.name.spineBandGingham': 'Bande vichy',
|
||||
'studio.templates.name.spineBandScript': 'Bande et script',
|
||||
'studio.templates.name.posterPopSunburst': 'Affiche rayonnante',
|
||||
'studio.templates.name.posterPopSplit': 'Affiche partagée',
|
||||
'studio.templates.name.posterPopStack': 'Affiche empilée',
|
||||
'studio.templates.name.cutoutMatisse': 'Papiers découpés',
|
||||
'studio.templates.name.cutoutArch': 'Arche découpée',
|
||||
'studio.templates.name.cutoutCollage': 'Collage découpé',
|
||||
'studio.templates.name.postcardHouses': 'Carte postale ville',
|
||||
'studio.templates.name.postcardCoast': 'Carte postale côte',
|
||||
'studio.templates.name.postcardPeaks': 'Carte postale montagne',
|
||||
'studio.templates.name.gridBadgeCircle': 'Grille et cercle',
|
||||
'studio.templates.name.gridBadgeStamp': 'Grille et tampon',
|
||||
'studio.templates.name.gridBadgeSeal': 'Grille et sceau',
|
||||
'studio.templates.name.minimalSerifClassic': 'Sérif classique',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Sérif portrait',
|
||||
'studio.templates.name.minimalSerifWindow': 'Sérif fenêtre',
|
||||
'studio.templates.name.stampCollageChecker': 'Collage à damier',
|
||||
'studio.templates.name.stampCollageRow': 'Rangée de timbres',
|
||||
'studio.templates.name.stampCollageArches': 'Arches sur damier',
|
||||
'studio.templates.name.heroScriptWord': 'Photo et script',
|
||||
'studio.templates.name.heroTrioWordart': 'Mot-image et trois photos',
|
||||
'studio.templates.name.storyQuote': 'Récit et citation',
|
||||
'studio.templates.name.bleedBandCaption': 'Panorama et bande',
|
||||
'studio.templates.name.twoToneSplit': 'Deux tons',
|
||||
'studio.templates.name.dayOpener': 'Ouverture du jour',
|
||||
'studio.templates.name.polaroidStack': 'Polaroïds',
|
||||
'studio.templates.name.stampRow': 'Timbres en ligne',
|
||||
'studio.templates.name.archGallery': 'Galerie d’arches',
|
||||
'studio.templates.name.mapTickets': 'Carte et billets',
|
||||
'studio.templates.name.filmstripScript': 'Pellicule',
|
||||
'studio.templates.name.postcardBack': 'Dos de carte postale',
|
||||
'studio.templates.name.gridTight': 'Grille serrée',
|
||||
'studio.templates.name.gridFeature': 'Grille à la une',
|
||||
'studio.templates.name.gridColumns': 'Grille en colonnes',
|
||||
'studio.templates.name.gridMosaic': 'Grille mosaïque',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Σχεδιασμένες σελίδες',
|
||||
'studio.templates.style': 'Στυλ',
|
||||
'studio.templates.all': 'Όλα',
|
||||
'studio.templates.hint':
|
||||
'Κάθε σχέδιο εμφανίζεται με τις φωτογραφίες και τα κείμενα αυτής της σελίδας. Με ένα κλικ η σελίδα στήνεται σύμφωνα με αυτό, και ό,τι δεν χωράει μένει με τη σελίδα.',
|
||||
'studio.templates.archetype.spine-band': 'Λωρίδα στη ράχη',
|
||||
'studio.templates.archetype.poster-pop': 'Αφίσα',
|
||||
'studio.templates.archetype.cutout': 'Χαρτοκοπτική',
|
||||
'studio.templates.archetype.postcard': 'Καρτ ποστάλ',
|
||||
'studio.templates.archetype.grid-badge': 'Πλέγμα και σήμα',
|
||||
'studio.templates.archetype.minimal-serif': 'Μινιμαλιστικό',
|
||||
'studio.templates.archetype.stamp-collage': 'Γραμματόσημα',
|
||||
'studio.templates.name.spineBandStitched': 'Ραμμένη λωρίδα',
|
||||
'studio.templates.name.spineBandGingham': 'Καρό λωρίδα',
|
||||
'studio.templates.name.spineBandScript': 'Λωρίδα και καλλιγραφικά',
|
||||
'studio.templates.name.posterPopSunburst': 'Αφίσα με ακτίνες',
|
||||
'studio.templates.name.posterPopSplit': 'Διαιρεμένη αφίσα',
|
||||
'studio.templates.name.posterPopStack': 'Στοιβαγμένη αφίσα',
|
||||
'studio.templates.name.cutoutMatisse': 'Χάρτινα κομμάτια',
|
||||
'studio.templates.name.cutoutArch': 'Κομμένη αψίδα',
|
||||
'studio.templates.name.cutoutCollage': 'Κολάζ από κομμάτια',
|
||||
'studio.templates.name.postcardHouses': 'Κάρτα με πόλη',
|
||||
'studio.templates.name.postcardCoast': 'Κάρτα με ακτή',
|
||||
'studio.templates.name.postcardPeaks': 'Κάρτα με βουνά',
|
||||
'studio.templates.name.gridBadgeCircle': 'Πλέγμα με κύκλο',
|
||||
'studio.templates.name.gridBadgeStamp': 'Πλέγμα με σφραγίδα',
|
||||
'studio.templates.name.gridBadgeSeal': 'Πλέγμα με βούλα',
|
||||
'studio.templates.name.minimalSerifClassic': 'Κλασική πατούρα',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Κάθετη πατούρα',
|
||||
'studio.templates.name.minimalSerifWindow': 'Πατούρα με παράθυρο',
|
||||
'studio.templates.name.stampCollageChecker': 'Κολάζ σε σκακιέρα',
|
||||
'studio.templates.name.stampCollageRow': 'Σειρά γραμματοσήμων',
|
||||
'studio.templates.name.stampCollageArches': 'Αψίδες σε σκακιέρα',
|
||||
'studio.templates.name.heroScriptWord': 'Φωτογραφία και καλλιγραφικά',
|
||||
'studio.templates.name.heroTrioWordart': 'Λέξη από φωτογραφία και τρεις',
|
||||
'studio.templates.name.storyQuote': 'Ιστορία με απόσπασμα',
|
||||
'studio.templates.name.bleedBandCaption': 'Πανόραμα με λωρίδα',
|
||||
'studio.templates.name.twoToneSplit': 'Δύο τόνοι',
|
||||
'studio.templates.name.dayOpener': 'Αρχή της ημέρας',
|
||||
'studio.templates.name.polaroidStack': 'Πολαρόιντ',
|
||||
'studio.templates.name.stampRow': 'Γραμματόσημα στη σειρά',
|
||||
'studio.templates.name.archGallery': 'Γκαλερί αψίδων',
|
||||
'studio.templates.name.mapTickets': 'Χάρτης και εισιτήρια',
|
||||
'studio.templates.name.filmstripScript': 'Φιλμ',
|
||||
'studio.templates.name.postcardBack': 'Πίσω όψη κάρτας',
|
||||
'studio.templates.name.gridTight': 'Πυκνό πλέγμα',
|
||||
'studio.templates.name.gridFeature': 'Πλέγμα με κεντρική φωτογραφία',
|
||||
'studio.templates.name.gridColumns': 'Πλέγμα σε στήλες',
|
||||
'studio.templates.name.gridMosaic': 'Πλέγμα μωσαϊκό',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Megtervezett oldalak',
|
||||
'studio.templates.style': 'Stílus',
|
||||
'studio.templates.all': 'Mind',
|
||||
'studio.templates.hint':
|
||||
'Minden terv ennek az oldalnak a fotóival és szövegeivel látszik. Egy kattintással az oldal ezt a tervet kapja, és ami nem fér el, az oldalnál marad.',
|
||||
'studio.templates.archetype.spine-band': 'Gerincszalag',
|
||||
'studio.templates.archetype.poster-pop': 'Plakát',
|
||||
'studio.templates.archetype.cutout': 'Papírkivágás',
|
||||
'studio.templates.archetype.postcard': 'Képeslap',
|
||||
'studio.templates.archetype.grid-badge': 'Rács és jelvény',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimál',
|
||||
'studio.templates.archetype.stamp-collage': 'Bélyegek',
|
||||
'studio.templates.name.spineBandStitched': 'Varrott szalag',
|
||||
'studio.templates.name.spineBandGingham': 'Kockás szalag',
|
||||
'studio.templates.name.spineBandScript': 'Szalag kézírással',
|
||||
'studio.templates.name.posterPopSunburst': 'Sugaras plakát',
|
||||
'studio.templates.name.posterPopSplit': 'Osztott plakát',
|
||||
'studio.templates.name.posterPopStack': 'Rétegzett plakát',
|
||||
'studio.templates.name.cutoutMatisse': 'Kivágott papírok',
|
||||
'studio.templates.name.cutoutArch': 'Kivágott ív',
|
||||
'studio.templates.name.cutoutCollage': 'Kivágás-kollázs',
|
||||
'studio.templates.name.postcardHouses': 'Városi képeslap',
|
||||
'studio.templates.name.postcardCoast': 'Tengerparti képeslap',
|
||||
'studio.templates.name.postcardPeaks': 'Hegyi képeslap',
|
||||
'studio.templates.name.gridBadgeCircle': 'Rács körrel',
|
||||
'studio.templates.name.gridBadgeStamp': 'Rács pecséttel',
|
||||
'studio.templates.name.gridBadgeSeal': 'Rács viaszpecséttel',
|
||||
'studio.templates.name.minimalSerifClassic': 'Klasszikus talpas',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Álló talpas',
|
||||
'studio.templates.name.minimalSerifWindow': 'Talpas ablakkal',
|
||||
'studio.templates.name.stampCollageChecker': 'Sakktáblás kollázs',
|
||||
'studio.templates.name.stampCollageRow': 'Bélyegsor',
|
||||
'studio.templates.name.stampCollageArches': 'Ívek sakktáblán',
|
||||
'studio.templates.name.heroScriptWord': 'Fotó és kézírás',
|
||||
'studio.templates.name.heroTrioWordart': 'Fotós szó és három fotó',
|
||||
'studio.templates.name.storyQuote': 'Történet idézettel',
|
||||
'studio.templates.name.bleedBandCaption': 'Panoráma szalaggal',
|
||||
'studio.templates.name.twoToneSplit': 'Két tónus',
|
||||
'studio.templates.name.dayOpener': 'Napnyitó',
|
||||
'studio.templates.name.polaroidStack': 'Polaroidok',
|
||||
'studio.templates.name.stampRow': 'Bélyegek sorban',
|
||||
'studio.templates.name.archGallery': 'Ívgaléria',
|
||||
'studio.templates.name.mapTickets': 'Térkép és jegyek',
|
||||
'studio.templates.name.filmstripScript': 'Filmszalag',
|
||||
'studio.templates.name.postcardBack': 'Képeslap hátoldala',
|
||||
'studio.templates.name.gridTight': 'Szoros rács',
|
||||
'studio.templates.name.gridFeature': 'Kiemelt rács',
|
||||
'studio.templates.name.gridColumns': 'Oszlopos rács',
|
||||
'studio.templates.name.gridMosaic': 'Mozaikrács',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Halaman berdesain',
|
||||
'studio.templates.style': 'Gaya',
|
||||
'studio.templates.all': 'Semua',
|
||||
'studio.templates.hint':
|
||||
'Setiap desain ditampilkan dengan foto dan teks halaman ini. Sekali klik menata halaman dengannya, dan yang tidak muat tetap tersimpan di halaman.',
|
||||
'studio.templates.archetype.spine-band': 'Pita punggung',
|
||||
'studio.templates.archetype.poster-pop': 'Poster', // same-as-en
|
||||
'studio.templates.archetype.cutout': 'Potongan kertas',
|
||||
'studio.templates.archetype.postcard': 'Kartu pos',
|
||||
'studio.templates.archetype.grid-badge': 'Kisi dan lencana',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimalis',
|
||||
'studio.templates.archetype.stamp-collage': 'Perangko',
|
||||
'studio.templates.name.spineBandStitched': 'Pita berjahit',
|
||||
'studio.templates.name.spineBandGingham': 'Pita kotak-kotak',
|
||||
'studio.templates.name.spineBandScript': 'Pita dan tulisan sambung',
|
||||
'studio.templates.name.posterPopSunburst': 'Poster bersinar',
|
||||
'studio.templates.name.posterPopSplit': 'Poster terbelah',
|
||||
'studio.templates.name.posterPopStack': 'Poster bertumpuk',
|
||||
'studio.templates.name.cutoutMatisse': 'Potongan kertas warna',
|
||||
'studio.templates.name.cutoutArch': 'Lengkung potongan',
|
||||
'studio.templates.name.cutoutCollage': 'Kolase potongan',
|
||||
'studio.templates.name.postcardHouses': 'Kartu pos kota',
|
||||
'studio.templates.name.postcardCoast': 'Kartu pos pantai',
|
||||
'studio.templates.name.postcardPeaks': 'Kartu pos gunung',
|
||||
'studio.templates.name.gridBadgeCircle': 'Kisi dengan lingkaran',
|
||||
'studio.templates.name.gridBadgeStamp': 'Kisi dengan cap',
|
||||
'studio.templates.name.gridBadgeSeal': 'Kisi dengan segel',
|
||||
'studio.templates.name.minimalSerifClassic': 'Serif klasik',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Serif tegak',
|
||||
'studio.templates.name.minimalSerifWindow': 'Serif berjendela',
|
||||
'studio.templates.name.stampCollageChecker': 'Kolase papan catur',
|
||||
'studio.templates.name.stampCollageRow': 'Deretan perangko',
|
||||
'studio.templates.name.stampCollageArches': 'Lengkung di papan catur',
|
||||
'studio.templates.name.heroScriptWord': 'Foto dan tulisan sambung',
|
||||
'studio.templates.name.heroTrioWordart': 'Kata bergambar dan tiga foto',
|
||||
'studio.templates.name.storyQuote': 'Cerita dan kutipan',
|
||||
'studio.templates.name.bleedBandCaption': 'Panorama berpita',
|
||||
'studio.templates.name.twoToneSplit': 'Dua warna',
|
||||
'studio.templates.name.dayOpener': 'Pembuka hari',
|
||||
'studio.templates.name.polaroidStack': 'Polaroid',
|
||||
'studio.templates.name.stampRow': 'Perangko berjajar',
|
||||
'studio.templates.name.archGallery': 'Galeri lengkung',
|
||||
'studio.templates.name.mapTickets': 'Peta dan tiket',
|
||||
'studio.templates.name.filmstripScript': 'Strip film',
|
||||
'studio.templates.name.postcardBack': 'Bagian belakang kartu pos',
|
||||
'studio.templates.name.gridTight': 'Kisi rapat',
|
||||
'studio.templates.name.gridFeature': 'Kisi unggulan',
|
||||
'studio.templates.name.gridColumns': 'Kisi kolom',
|
||||
'studio.templates.name.gridMosaic': 'Kisi mozaik',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Pagine progettate',
|
||||
'studio.templates.style': 'Stile',
|
||||
'studio.templates.all': 'Tutti',
|
||||
'studio.templates.hint':
|
||||
'Ogni modello è disegnato con le foto e i testi di questa pagina. Un clic impagina la pagina così, e ciò che non trova posto resta con la pagina.',
|
||||
'studio.templates.archetype.spine-band': 'Fascia sul dorso',
|
||||
'studio.templates.archetype.poster-pop': 'Poster', // same-as-en
|
||||
'studio.templates.archetype.cutout': 'Ritagli',
|
||||
'studio.templates.archetype.postcard': 'Cartolina',
|
||||
'studio.templates.archetype.grid-badge': 'Griglia e distintivo',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimal', // same-as-en
|
||||
'studio.templates.archetype.stamp-collage': 'Francobolli',
|
||||
'studio.templates.name.spineBandStitched': 'Fascia cucita',
|
||||
'studio.templates.name.spineBandGingham': 'Fascia a quadretti',
|
||||
'studio.templates.name.spineBandScript': 'Fascia e corsivo',
|
||||
'studio.templates.name.posterPopSunburst': 'Poster a raggiera',
|
||||
'studio.templates.name.posterPopSplit': 'Poster diviso',
|
||||
'studio.templates.name.posterPopStack': 'Poster impilato',
|
||||
'studio.templates.name.cutoutMatisse': 'Carte ritagliate',
|
||||
'studio.templates.name.cutoutArch': 'Arco ritagliato',
|
||||
'studio.templates.name.cutoutCollage': 'Collage di ritagli',
|
||||
'studio.templates.name.postcardHouses': 'Cartolina di città',
|
||||
'studio.templates.name.postcardCoast': 'Cartolina di costa',
|
||||
'studio.templates.name.postcardPeaks': 'Cartolina di montagna',
|
||||
'studio.templates.name.gridBadgeCircle': 'Griglia con cerchio',
|
||||
'studio.templates.name.gridBadgeStamp': 'Griglia con timbro',
|
||||
'studio.templates.name.gridBadgeSeal': 'Griglia con sigillo',
|
||||
'studio.templates.name.minimalSerifClassic': 'Graziato classico',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Graziato verticale',
|
||||
'studio.templates.name.minimalSerifWindow': 'Graziato a finestra',
|
||||
'studio.templates.name.stampCollageChecker': 'Collage a scacchi',
|
||||
'studio.templates.name.stampCollageRow': 'Fila di francobolli',
|
||||
'studio.templates.name.stampCollageArches': 'Archi su scacchi',
|
||||
'studio.templates.name.heroScriptWord': 'Foto e corsivo',
|
||||
'studio.templates.name.heroTrioWordart': 'Parola immagine e tre foto',
|
||||
'studio.templates.name.storyQuote': 'Racconto e citazione',
|
||||
'studio.templates.name.bleedBandCaption': 'Panorama con fascia',
|
||||
'studio.templates.name.twoToneSplit': 'Due toni',
|
||||
'studio.templates.name.dayOpener': 'Apertura del giorno',
|
||||
'studio.templates.name.polaroidStack': 'Polaroid',
|
||||
'studio.templates.name.stampRow': 'Francobolli in fila',
|
||||
'studio.templates.name.archGallery': 'Galleria di archi',
|
||||
'studio.templates.name.mapTickets': 'Mappa e biglietti',
|
||||
'studio.templates.name.filmstripScript': 'Pellicola',
|
||||
'studio.templates.name.postcardBack': 'Retro di cartolina',
|
||||
'studio.templates.name.gridTight': 'Griglia stretta',
|
||||
'studio.templates.name.gridFeature': 'Griglia in evidenza',
|
||||
'studio.templates.name.gridColumns': 'Griglia a colonne',
|
||||
'studio.templates.name.gridMosaic': 'Griglia a mosaico',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'デザイン済みページ',
|
||||
'studio.templates.style': 'スタイル',
|
||||
'studio.templates.all': 'すべて',
|
||||
'studio.templates.hint':
|
||||
'各デザインはこのページの写真と文字で表示されます。クリックするとページがそのデザインで組まれ、収まらないものはページに残ります。',
|
||||
'studio.templates.archetype.spine-band': '背の帯',
|
||||
'studio.templates.archetype.poster-pop': 'ポスター',
|
||||
'studio.templates.archetype.cutout': '切り絵',
|
||||
'studio.templates.archetype.postcard': 'ポストカード',
|
||||
'studio.templates.archetype.grid-badge': 'グリッドとバッジ',
|
||||
'studio.templates.archetype.minimal-serif': 'ミニマル',
|
||||
'studio.templates.archetype.stamp-collage': '切手',
|
||||
'studio.templates.name.spineBandStitched': 'ステッチの帯',
|
||||
'studio.templates.name.spineBandGingham': 'ギンガムの帯',
|
||||
'studio.templates.name.spineBandScript': '帯と筆記体',
|
||||
'studio.templates.name.posterPopSunburst': '放射線のポスター',
|
||||
'studio.templates.name.posterPopSplit': '二分割のポスター',
|
||||
'studio.templates.name.posterPopStack': '重ねたポスター',
|
||||
'studio.templates.name.cutoutMatisse': '紙の切り絵',
|
||||
'studio.templates.name.cutoutArch': '切り抜きのアーチ',
|
||||
'studio.templates.name.cutoutCollage': '切り絵のコラージュ',
|
||||
'studio.templates.name.postcardHouses': '街のポストカード',
|
||||
'studio.templates.name.postcardCoast': '海辺のポストカード',
|
||||
'studio.templates.name.postcardPeaks': '山のポストカード',
|
||||
'studio.templates.name.gridBadgeCircle': '円のあるグリッド',
|
||||
'studio.templates.name.gridBadgeStamp': 'スタンプのあるグリッド',
|
||||
'studio.templates.name.gridBadgeSeal': 'シールのあるグリッド',
|
||||
'studio.templates.name.minimalSerifClassic': 'クラシックなセリフ',
|
||||
'studio.templates.name.minimalSerifPortrait': '縦長のセリフ',
|
||||
'studio.templates.name.minimalSerifWindow': '窓のあるセリフ',
|
||||
'studio.templates.name.stampCollageChecker': '市松模様のコラージュ',
|
||||
'studio.templates.name.stampCollageRow': '切手の列',
|
||||
'studio.templates.name.stampCollageArches': '市松模様のアーチ',
|
||||
'studio.templates.name.heroScriptWord': '写真と筆記体',
|
||||
'studio.templates.name.heroTrioWordart': '写真文字と三枚の写真',
|
||||
'studio.templates.name.storyQuote': '物語と引用',
|
||||
'studio.templates.name.bleedBandCaption': '帯のあるパノラマ',
|
||||
'studio.templates.name.twoToneSplit': '二色',
|
||||
'studio.templates.name.dayOpener': '一日の始まり',
|
||||
'studio.templates.name.polaroidStack': 'ポラロイド',
|
||||
'studio.templates.name.stampRow': '並んだ切手',
|
||||
'studio.templates.name.archGallery': 'アーチのギャラリー',
|
||||
'studio.templates.name.mapTickets': '地図とチケット',
|
||||
'studio.templates.name.filmstripScript': 'フィルムストリップ',
|
||||
'studio.templates.name.postcardBack': 'ポストカードの裏面',
|
||||
'studio.templates.name.gridTight': '詰まったグリッド',
|
||||
'studio.templates.name.gridFeature': '主役のあるグリッド',
|
||||
'studio.templates.name.gridColumns': '列のグリッド',
|
||||
'studio.templates.name.gridMosaic': 'モザイクのグリッド',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': '디자인된 페이지',
|
||||
'studio.templates.style': '스타일',
|
||||
'studio.templates.all': '전체',
|
||||
'studio.templates.hint':
|
||||
'각 디자인은 이 페이지의 사진과 글로 그려집니다. 클릭하면 페이지가 그 디자인으로 배치되고, 들어갈 자리가 없는 것은 페이지에 보관됩니다.',
|
||||
'studio.templates.archetype.spine-band': '책등 띠',
|
||||
'studio.templates.archetype.poster-pop': '포스터',
|
||||
'studio.templates.archetype.cutout': '오려 붙이기',
|
||||
'studio.templates.archetype.postcard': '엽서',
|
||||
'studio.templates.archetype.grid-badge': '격자와 배지',
|
||||
'studio.templates.archetype.minimal-serif': '미니멀',
|
||||
'studio.templates.archetype.stamp-collage': '우표',
|
||||
'studio.templates.name.spineBandStitched': '박음질 띠',
|
||||
'studio.templates.name.spineBandGingham': '깅엄 띠',
|
||||
'studio.templates.name.spineBandScript': '띠와 손글씨',
|
||||
'studio.templates.name.posterPopSunburst': '햇살 포스터',
|
||||
'studio.templates.name.posterPopSplit': '나뉜 포스터',
|
||||
'studio.templates.name.posterPopStack': '겹친 포스터',
|
||||
'studio.templates.name.cutoutMatisse': '종이 오리기',
|
||||
'studio.templates.name.cutoutArch': '오려 낸 아치',
|
||||
'studio.templates.name.cutoutCollage': '오려 붙인 콜라주',
|
||||
'studio.templates.name.postcardHouses': '도시 엽서',
|
||||
'studio.templates.name.postcardCoast': '해안 엽서',
|
||||
'studio.templates.name.postcardPeaks': '산 엽서',
|
||||
'studio.templates.name.gridBadgeCircle': '원이 있는 격자',
|
||||
'studio.templates.name.gridBadgeStamp': '스탬프가 있는 격자',
|
||||
'studio.templates.name.gridBadgeSeal': '씰이 있는 격자',
|
||||
'studio.templates.name.minimalSerifClassic': '클래식 세리프',
|
||||
'studio.templates.name.minimalSerifPortrait': '세로 세리프',
|
||||
'studio.templates.name.minimalSerifWindow': '창이 있는 세리프',
|
||||
'studio.templates.name.stampCollageChecker': '체크무늬 콜라주',
|
||||
'studio.templates.name.stampCollageRow': '우표 한 줄',
|
||||
'studio.templates.name.stampCollageArches': '체크무늬 위 아치',
|
||||
'studio.templates.name.heroScriptWord': '사진과 손글씨',
|
||||
'studio.templates.name.heroTrioWordart': '사진 글자와 사진 세 장',
|
||||
'studio.templates.name.storyQuote': '이야기와 인용',
|
||||
'studio.templates.name.bleedBandCaption': '띠가 있는 파노라마',
|
||||
'studio.templates.name.twoToneSplit': '두 가지 색',
|
||||
'studio.templates.name.dayOpener': '하루의 시작',
|
||||
'studio.templates.name.polaroidStack': '폴라로이드',
|
||||
'studio.templates.name.stampRow': '나란한 우표',
|
||||
'studio.templates.name.archGallery': '아치 갤러리',
|
||||
'studio.templates.name.mapTickets': '지도와 티켓',
|
||||
'studio.templates.name.filmstripScript': '필름 띠',
|
||||
'studio.templates.name.postcardBack': '엽서 뒷면',
|
||||
'studio.templates.name.gridTight': '촘촘한 격자',
|
||||
'studio.templates.name.gridFeature': '주인공 격자',
|
||||
'studio.templates.name.gridColumns': '단 격자',
|
||||
'studio.templates.name.gridMosaic': '모자이크 격자',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Ontworpen pagina’s',
|
||||
'studio.templates.style': 'Stijl',
|
||||
'studio.templates.all': 'Alle',
|
||||
'studio.templates.hint':
|
||||
'Elk ontwerp toont de foto’s en teksten van deze pagina. Met een klik krijgt de pagina deze opmaak, en wat geen plek heeft, blijft bij de pagina.',
|
||||
'studio.templates.archetype.spine-band': 'Rugband',
|
||||
'studio.templates.archetype.poster-pop': 'Poster', // same-as-en
|
||||
'studio.templates.archetype.cutout': 'Knipwerk',
|
||||
'studio.templates.archetype.postcard': 'Ansichtkaart',
|
||||
'studio.templates.archetype.grid-badge': 'Raster en badge',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimaal',
|
||||
'studio.templates.archetype.stamp-collage': 'Postzegels',
|
||||
'studio.templates.name.spineBandStitched': 'Gestikte band',
|
||||
'studio.templates.name.spineBandGingham': 'Band met ruitjes',
|
||||
'studio.templates.name.spineBandScript': 'Band met handschrift',
|
||||
'studio.templates.name.posterPopSunburst': 'Poster met stralen',
|
||||
'studio.templates.name.posterPopSplit': 'Gedeelde poster',
|
||||
'studio.templates.name.posterPopStack': 'Gestapelde poster',
|
||||
'studio.templates.name.cutoutMatisse': 'Papierknipsels',
|
||||
'studio.templates.name.cutoutArch': 'Geknipte boog',
|
||||
'studio.templates.name.cutoutCollage': 'Knipcollage',
|
||||
'studio.templates.name.postcardHouses': 'Ansicht met stad',
|
||||
'studio.templates.name.postcardCoast': 'Ansicht met kust',
|
||||
'studio.templates.name.postcardPeaks': 'Ansicht met bergen',
|
||||
'studio.templates.name.gridBadgeCircle': 'Raster met cirkel',
|
||||
'studio.templates.name.gridBadgeStamp': 'Raster met stempel',
|
||||
'studio.templates.name.gridBadgeSeal': 'Raster met zegel',
|
||||
'studio.templates.name.minimalSerifClassic': 'Klassieke schreef',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Staande schreef',
|
||||
'studio.templates.name.minimalSerifWindow': 'Schreef met venster',
|
||||
'studio.templates.name.stampCollageChecker': 'Collage met dambord',
|
||||
'studio.templates.name.stampCollageRow': 'Rij postzegels',
|
||||
'studio.templates.name.stampCollageArches': 'Bogen op dambord',
|
||||
'studio.templates.name.heroScriptWord': 'Foto met handschrift',
|
||||
'studio.templates.name.heroTrioWordart': 'Woordbeeld en drie foto’s',
|
||||
'studio.templates.name.storyQuote': 'Verhaal met citaat',
|
||||
'studio.templates.name.bleedBandCaption': 'Panorama met band',
|
||||
'studio.templates.name.twoToneSplit': 'Twee tinten',
|
||||
'studio.templates.name.dayOpener': 'Opening van de dag',
|
||||
'studio.templates.name.polaroidStack': 'Polaroids', // same-as-en
|
||||
'studio.templates.name.stampRow': 'Postzegels op een rij',
|
||||
'studio.templates.name.archGallery': 'Bogengalerij',
|
||||
'studio.templates.name.mapTickets': 'Kaart en tickets',
|
||||
'studio.templates.name.filmstripScript': 'Filmstrook',
|
||||
'studio.templates.name.postcardBack': 'Achterkant van een ansicht',
|
||||
'studio.templates.name.gridTight': 'Strak raster',
|
||||
'studio.templates.name.gridFeature': 'Raster met blikvanger',
|
||||
'studio.templates.name.gridColumns': 'Kolommenraster',
|
||||
'studio.templates.name.gridMosaic': 'Mozaïekraster',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Zaprojektowane strony',
|
||||
'studio.templates.style': 'Styl',
|
||||
'studio.templates.all': 'Wszystkie',
|
||||
'studio.templates.hint':
|
||||
'Każdy projekt pokazuje zdjęcia i teksty tej strony. Kliknięcie układa stronę według niego, a to, co się nie mieści, zostaje przy stronie.',
|
||||
'studio.templates.archetype.spine-band': 'Pas na grzbiecie',
|
||||
'studio.templates.archetype.poster-pop': 'Plakat',
|
||||
'studio.templates.archetype.cutout': 'Wycinanki',
|
||||
'studio.templates.archetype.postcard': 'Pocztówka',
|
||||
'studio.templates.archetype.grid-badge': 'Siatka i plakietka',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimalizm',
|
||||
'studio.templates.archetype.stamp-collage': 'Znaczki',
|
||||
'studio.templates.name.spineBandStitched': 'Przeszyty pas',
|
||||
'studio.templates.name.spineBandGingham': 'Pas w kratkę',
|
||||
'studio.templates.name.spineBandScript': 'Pas z pismem odręcznym',
|
||||
'studio.templates.name.posterPopSunburst': 'Plakat z promieniami',
|
||||
'studio.templates.name.posterPopSplit': 'Plakat dzielony',
|
||||
'studio.templates.name.posterPopStack': 'Plakat warstwowy',
|
||||
'studio.templates.name.cutoutMatisse': 'Papierowe wycinanki',
|
||||
'studio.templates.name.cutoutArch': 'Wycięty łuk',
|
||||
'studio.templates.name.cutoutCollage': 'Kolaż z wycinanek',
|
||||
'studio.templates.name.postcardHouses': 'Pocztówka z miastem',
|
||||
'studio.templates.name.postcardCoast': 'Pocztówka z wybrzeżem',
|
||||
'studio.templates.name.postcardPeaks': 'Pocztówka z górami',
|
||||
'studio.templates.name.gridBadgeCircle': 'Siatka z kołem',
|
||||
'studio.templates.name.gridBadgeStamp': 'Siatka ze stemplem',
|
||||
'studio.templates.name.gridBadgeSeal': 'Siatka z pieczęcią',
|
||||
'studio.templates.name.minimalSerifClassic': 'Klasyczny szeryf',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Szeryf w pionie',
|
||||
'studio.templates.name.minimalSerifWindow': 'Szeryf z oknem',
|
||||
'studio.templates.name.stampCollageChecker': 'Kolaż w szachownicę',
|
||||
'studio.templates.name.stampCollageRow': 'Rząd znaczków',
|
||||
'studio.templates.name.stampCollageArches': 'Łuki na szachownicy',
|
||||
'studio.templates.name.heroScriptWord': 'Zdjęcie i pismo odręczne',
|
||||
'studio.templates.name.heroTrioWordart': 'Słowo ze zdjęcia i trzy zdjęcia',
|
||||
'studio.templates.name.storyQuote': 'Opowieść z cytatem',
|
||||
'studio.templates.name.bleedBandCaption': 'Panorama z pasem',
|
||||
'studio.templates.name.twoToneSplit': 'Dwa kolory',
|
||||
'studio.templates.name.dayOpener': 'Początek dnia',
|
||||
'studio.templates.name.polaroidStack': 'Polaroidy',
|
||||
'studio.templates.name.stampRow': 'Znaczki w rzędzie',
|
||||
'studio.templates.name.archGallery': 'Galeria łuków',
|
||||
'studio.templates.name.mapTickets': 'Mapa i bilety',
|
||||
'studio.templates.name.filmstripScript': 'Taśma filmowa',
|
||||
'studio.templates.name.postcardBack': 'Rewers pocztówki',
|
||||
'studio.templates.name.gridTight': 'Ciasna siatka',
|
||||
'studio.templates.name.gridFeature': 'Siatka z głównym zdjęciem',
|
||||
'studio.templates.name.gridColumns': 'Siatka kolumn',
|
||||
'studio.templates.name.gridMosaic': 'Siatka mozaikowa',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Оформленные страницы',
|
||||
'studio.templates.style': 'Стиль',
|
||||
'studio.templates.all': 'Все',
|
||||
'studio.templates.hint':
|
||||
'Каждый макет показан с фотографиями и текстами этой страницы. По щелчку страница получает этот макет, а то, чему не нашлось места, остаётся при странице.',
|
||||
'studio.templates.archetype.spine-band': 'Полоса у корешка',
|
||||
'studio.templates.archetype.poster-pop': 'Плакат',
|
||||
'studio.templates.archetype.cutout': 'Аппликация',
|
||||
'studio.templates.archetype.postcard': 'Открытка',
|
||||
'studio.templates.archetype.grid-badge': 'Сетка и значок',
|
||||
'studio.templates.archetype.minimal-serif': 'Минимализм',
|
||||
'studio.templates.archetype.stamp-collage': 'Марки',
|
||||
'studio.templates.name.spineBandStitched': 'Прошитая полоса',
|
||||
'studio.templates.name.spineBandGingham': 'Полоса в клетку',
|
||||
'studio.templates.name.spineBandScript': 'Полоса и рукописный шрифт',
|
||||
'studio.templates.name.posterPopSunburst': 'Плакат с лучами',
|
||||
'studio.templates.name.posterPopSplit': 'Разделённый плакат',
|
||||
'studio.templates.name.posterPopStack': 'Многоярусный плакат',
|
||||
'studio.templates.name.cutoutMatisse': 'Бумажная аппликация',
|
||||
'studio.templates.name.cutoutArch': 'Вырезанная арка',
|
||||
'studio.templates.name.cutoutCollage': 'Коллаж из вырезок',
|
||||
'studio.templates.name.postcardHouses': 'Открытка с городом',
|
||||
'studio.templates.name.postcardCoast': 'Открытка с побережьем',
|
||||
'studio.templates.name.postcardPeaks': 'Открытка с горами',
|
||||
'studio.templates.name.gridBadgeCircle': 'Сетка с кругом',
|
||||
'studio.templates.name.gridBadgeStamp': 'Сетка со штампом',
|
||||
'studio.templates.name.gridBadgeSeal': 'Сетка с печатью',
|
||||
'studio.templates.name.minimalSerifClassic': 'Классическая антиква',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Вертикальная антиква',
|
||||
'studio.templates.name.minimalSerifWindow': 'Антиква с окном',
|
||||
'studio.templates.name.stampCollageChecker': 'Коллаж в клетку',
|
||||
'studio.templates.name.stampCollageRow': 'Ряд марок',
|
||||
'studio.templates.name.stampCollageArches': 'Арки на клетке',
|
||||
'studio.templates.name.heroScriptWord': 'Фото и рукописный шрифт',
|
||||
'studio.templates.name.heroTrioWordart': 'Слово из фото и три фото',
|
||||
'studio.templates.name.storyQuote': 'Рассказ с цитатой',
|
||||
'studio.templates.name.bleedBandCaption': 'Панорама с полосой',
|
||||
'studio.templates.name.twoToneSplit': 'Два цвета',
|
||||
'studio.templates.name.dayOpener': 'Начало дня',
|
||||
'studio.templates.name.polaroidStack': 'Полароиды',
|
||||
'studio.templates.name.stampRow': 'Марки в ряд',
|
||||
'studio.templates.name.archGallery': 'Галерея арок',
|
||||
'studio.templates.name.mapTickets': 'Карта и билеты',
|
||||
'studio.templates.name.filmstripScript': 'Киноплёнка',
|
||||
'studio.templates.name.postcardBack': 'Оборот открытки',
|
||||
'studio.templates.name.gridTight': 'Плотная сетка',
|
||||
'studio.templates.name.gridFeature': 'Сетка с главным фото',
|
||||
'studio.templates.name.gridColumns': 'Сетка колонками',
|
||||
'studio.templates.name.gridMosaic': 'Мозаичная сетка',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Navrhnuté strany',
|
||||
'studio.templates.style': 'Štýl',
|
||||
'studio.templates.all': 'Všetko',
|
||||
'studio.templates.hint':
|
||||
'Každý návrh sa zobrazuje s fotkami a textami tejto strany. Kliknutím stranu podľa neho rozvrhnete, a čo sa nezmestí, zostane pri strane.',
|
||||
'studio.templates.archetype.spine-band': 'Pás na chrbte',
|
||||
'studio.templates.archetype.poster-pop': 'Plagát',
|
||||
'studio.templates.archetype.cutout': 'Vystrihovačky',
|
||||
'studio.templates.archetype.postcard': 'Pohľadnica',
|
||||
'studio.templates.archetype.grid-badge': 'Mriežka a odznak',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimalizmus',
|
||||
'studio.templates.archetype.stamp-collage': 'Známky',
|
||||
'studio.templates.name.spineBandStitched': 'Prešívaný pás',
|
||||
'studio.templates.name.spineBandGingham': 'Kockovaný pás',
|
||||
'studio.templates.name.spineBandScript': 'Pás s písaným písmom',
|
||||
'studio.templates.name.posterPopSunburst': 'Plagát s lúčmi',
|
||||
'studio.templates.name.posterPopSplit': 'Rozdelený plagát',
|
||||
'studio.templates.name.posterPopStack': 'Vrstvený plagát',
|
||||
'studio.templates.name.cutoutMatisse': 'Papierové výstrižky',
|
||||
'studio.templates.name.cutoutArch': 'Vystrihnutý oblúk',
|
||||
'studio.templates.name.cutoutCollage': 'Koláž z výstrižkov',
|
||||
'studio.templates.name.postcardHouses': 'Pohľadnica s mestom',
|
||||
'studio.templates.name.postcardCoast': 'Pohľadnica s pobrežím',
|
||||
'studio.templates.name.postcardPeaks': 'Pohľadnica s horami',
|
||||
'studio.templates.name.gridBadgeCircle': 'Mriežka s kruhom',
|
||||
'studio.templates.name.gridBadgeStamp': 'Mriežka s pečiatkou',
|
||||
'studio.templates.name.gridBadgeSeal': 'Mriežka s pečaťou',
|
||||
'studio.templates.name.minimalSerifClassic': 'Klasická pätková',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Pätková na výšku',
|
||||
'studio.templates.name.minimalSerifWindow': 'Pätková s oknom',
|
||||
'studio.templates.name.stampCollageChecker': 'Šachovnicová koláž',
|
||||
'studio.templates.name.stampCollageRow': 'Rad známok',
|
||||
'studio.templates.name.stampCollageArches': 'Oblúky na šachovnici',
|
||||
'studio.templates.name.heroScriptWord': 'Fotka a písané písmo',
|
||||
'studio.templates.name.heroTrioWordart': 'Slovo z fotky a tri fotky',
|
||||
'studio.templates.name.storyQuote': 'Príbeh s citátom',
|
||||
'studio.templates.name.bleedBandCaption': 'Panoráma s pásom',
|
||||
'studio.templates.name.twoToneSplit': 'Dve farby',
|
||||
'studio.templates.name.dayOpener': 'Úvod dňa',
|
||||
'studio.templates.name.polaroidStack': 'Polaroidy',
|
||||
'studio.templates.name.stampRow': 'Známky v rade',
|
||||
'studio.templates.name.archGallery': 'Galéria oblúkov',
|
||||
'studio.templates.name.mapTickets': 'Mapa a lístky',
|
||||
'studio.templates.name.filmstripScript': 'Filmový pás',
|
||||
'studio.templates.name.postcardBack': 'Rub pohľadnice',
|
||||
'studio.templates.name.gridTight': 'Tesná mriežka',
|
||||
'studio.templates.name.gridFeature': 'Mriežka s hlavnou fotkou',
|
||||
'studio.templates.name.gridColumns': 'Stĺpcová mriežka',
|
||||
'studio.templates.name.gridMosaic': 'Mozaiková mriežka',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Formgivna sidor',
|
||||
'studio.templates.style': 'Stil',
|
||||
'studio.templates.all': 'Alla',
|
||||
'studio.templates.hint':
|
||||
'Varje formgivning visas med den här sidans foton och texter. Ett klick ger sidan den formen, och det som inte får plats stannar kvar på sidan.',
|
||||
'studio.templates.archetype.spine-band': 'Ryggband',
|
||||
'studio.templates.archetype.poster-pop': 'Affisch',
|
||||
'studio.templates.archetype.cutout': 'Pappersklipp',
|
||||
'studio.templates.archetype.postcard': 'Vykort',
|
||||
'studio.templates.archetype.grid-badge': 'Rutnät och märke',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimal', // same-as-en
|
||||
'studio.templates.archetype.stamp-collage': 'Frimärken',
|
||||
'studio.templates.name.spineBandStitched': 'Sytt band',
|
||||
'studio.templates.name.spineBandGingham': 'Rutigt band',
|
||||
'studio.templates.name.spineBandScript': 'Band med skrivstil',
|
||||
'studio.templates.name.posterPopSunburst': 'Affisch med strålar',
|
||||
'studio.templates.name.posterPopSplit': 'Delad affisch',
|
||||
'studio.templates.name.posterPopStack': 'Staplad affisch',
|
||||
'studio.templates.name.cutoutMatisse': 'Klippta papper',
|
||||
'studio.templates.name.cutoutArch': 'Klippt valv',
|
||||
'studio.templates.name.cutoutCollage': 'Klippcollage',
|
||||
'studio.templates.name.postcardHouses': 'Vykort med stad',
|
||||
'studio.templates.name.postcardCoast': 'Vykort med kust',
|
||||
'studio.templates.name.postcardPeaks': 'Vykort med berg',
|
||||
'studio.templates.name.gridBadgeCircle': 'Rutnät med cirkel',
|
||||
'studio.templates.name.gridBadgeStamp': 'Rutnät med stämpel',
|
||||
'studio.templates.name.gridBadgeSeal': 'Rutnät med sigill',
|
||||
'studio.templates.name.minimalSerifClassic': 'Klassisk antikva',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Stående antikva',
|
||||
'studio.templates.name.minimalSerifWindow': 'Antikva med fönster',
|
||||
'studio.templates.name.stampCollageChecker': 'Rutigt collage',
|
||||
'studio.templates.name.stampCollageRow': 'Rad med frimärken',
|
||||
'studio.templates.name.stampCollageArches': 'Valv på rutor',
|
||||
'studio.templates.name.heroScriptWord': 'Foto och skrivstil',
|
||||
'studio.templates.name.heroTrioWordart': 'Ordbild och tre foton',
|
||||
'studio.templates.name.storyQuote': 'Berättelse och citat',
|
||||
'studio.templates.name.bleedBandCaption': 'Panorama med band',
|
||||
'studio.templates.name.twoToneSplit': 'Två toner',
|
||||
'studio.templates.name.dayOpener': 'Dagens öppning',
|
||||
'studio.templates.name.polaroidStack': 'Polaroider',
|
||||
'studio.templates.name.stampRow': 'Frimärken i rad',
|
||||
'studio.templates.name.archGallery': 'Valvgalleri',
|
||||
'studio.templates.name.mapTickets': 'Karta och biljetter',
|
||||
'studio.templates.name.filmstripScript': 'Filmremsa',
|
||||
'studio.templates.name.postcardBack': 'Vykortets baksida',
|
||||
'studio.templates.name.gridTight': 'Tätt rutnät',
|
||||
'studio.templates.name.gridFeature': 'Rutnät med huvudbild',
|
||||
'studio.templates.name.gridColumns': 'Kolumnrutnät',
|
||||
'studio.templates.name.gridMosaic': 'Mosaikrutnät',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'หน้าที่ออกแบบไว้',
|
||||
'studio.templates.style': 'สไตล์',
|
||||
'studio.templates.all': 'ทั้งหมด',
|
||||
'studio.templates.hint':
|
||||
'แต่ละแบบจะแสดงด้วยรูปภาพและข้อความของหน้านี้ คลิกเดียวจัดหน้าตามแบบนั้น ส่วนที่ไม่มีที่วางจะเก็บไว้กับหน้า',
|
||||
'studio.templates.archetype.spine-band': 'แถบสันปก',
|
||||
'studio.templates.archetype.poster-pop': 'โปสเตอร์',
|
||||
'studio.templates.archetype.cutout': 'กระดาษตัด',
|
||||
'studio.templates.archetype.postcard': 'โปสการ์ด',
|
||||
'studio.templates.archetype.grid-badge': 'ตารางและเหรียญตรา',
|
||||
'studio.templates.archetype.minimal-serif': 'มินิมอล',
|
||||
'studio.templates.archetype.stamp-collage': 'แสตมป์',
|
||||
'studio.templates.name.spineBandStitched': 'แถบเย็บ',
|
||||
'studio.templates.name.spineBandGingham': 'แถบลายตาราง',
|
||||
'studio.templates.name.spineBandScript': 'แถบกับลายมือ',
|
||||
'studio.templates.name.posterPopSunburst': 'โปสเตอร์รัศมี',
|
||||
'studio.templates.name.posterPopSplit': 'โปสเตอร์แบ่งครึ่ง',
|
||||
'studio.templates.name.posterPopStack': 'โปสเตอร์ซ้อนชั้น',
|
||||
'studio.templates.name.cutoutMatisse': 'กระดาษตัดแปะ',
|
||||
'studio.templates.name.cutoutArch': 'ซุ้มโค้งตัดกระดาษ',
|
||||
'studio.templates.name.cutoutCollage': 'คอลลาจกระดาษตัด',
|
||||
'studio.templates.name.postcardHouses': 'โปสการ์ดเมือง',
|
||||
'studio.templates.name.postcardCoast': 'โปสการ์ดชายฝั่ง',
|
||||
'studio.templates.name.postcardPeaks': 'โปสการ์ดภูเขา',
|
||||
'studio.templates.name.gridBadgeCircle': 'ตารางกับวงกลม',
|
||||
'studio.templates.name.gridBadgeStamp': 'ตารางกับตราประทับ',
|
||||
'studio.templates.name.gridBadgeSeal': 'ตารางกับตราผนึก',
|
||||
'studio.templates.name.minimalSerifClassic': 'เซอริฟคลาสสิก',
|
||||
'studio.templates.name.minimalSerifPortrait': 'เซอริฟแนวตั้ง',
|
||||
'studio.templates.name.minimalSerifWindow': 'เซอริฟกับหน้าต่าง',
|
||||
'studio.templates.name.stampCollageChecker': 'คอลลาจตารางหมากรุก',
|
||||
'studio.templates.name.stampCollageRow': 'แถวแสตมป์',
|
||||
'studio.templates.name.stampCollageArches': 'ซุ้มโค้งบนตารางหมากรุก',
|
||||
'studio.templates.name.heroScriptWord': 'รูปภาพกับลายมือ',
|
||||
'studio.templates.name.heroTrioWordart': 'คำจากรูปภาพกับสามรูป',
|
||||
'studio.templates.name.storyQuote': 'เรื่องเล่ากับคำพูด',
|
||||
'studio.templates.name.bleedBandCaption': 'พาโนรามากับแถบ',
|
||||
'studio.templates.name.twoToneSplit': 'สองโทน',
|
||||
'studio.templates.name.dayOpener': 'เปิดวันใหม่',
|
||||
'studio.templates.name.polaroidStack': 'โพลารอยด์',
|
||||
'studio.templates.name.stampRow': 'แสตมป์เรียงกัน',
|
||||
'studio.templates.name.archGallery': 'แกลเลอรีซุ้มโค้ง',
|
||||
'studio.templates.name.mapTickets': 'แผนที่กับตั๋ว',
|
||||
'studio.templates.name.filmstripScript': 'แถบฟิล์ม',
|
||||
'studio.templates.name.postcardBack': 'ด้านหลังโปสการ์ด',
|
||||
'studio.templates.name.gridTight': 'ตารางแน่น',
|
||||
'studio.templates.name.gridFeature': 'ตารางภาพเด่น',
|
||||
'studio.templates.name.gridColumns': 'ตารางคอลัมน์',
|
||||
'studio.templates.name.gridMosaic': 'ตารางโมเสก',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Tasarlanmış sayfalar',
|
||||
'studio.templates.style': 'Stil',
|
||||
'studio.templates.all': 'Tümü',
|
||||
'studio.templates.hint':
|
||||
'Her tasarım bu sayfanın fotoğrafları ve yazılarıyla gösterilir. Bir tıklama sayfayı ona göre düzenler, sığmayanlar da sayfada kalır.',
|
||||
'studio.templates.archetype.spine-band': 'Sırt bandı',
|
||||
'studio.templates.archetype.poster-pop': 'Poster', // same-as-en
|
||||
'studio.templates.archetype.cutout': 'Kesik kâğıt',
|
||||
'studio.templates.archetype.postcard': 'Kartpostal',
|
||||
'studio.templates.archetype.grid-badge': 'Izgara ve rozet',
|
||||
'studio.templates.archetype.minimal-serif': 'Minimal', // same-as-en
|
||||
'studio.templates.archetype.stamp-collage': 'Pullar',
|
||||
'studio.templates.name.spineBandStitched': 'Dikişli bant',
|
||||
'studio.templates.name.spineBandGingham': 'Kareli bant',
|
||||
'studio.templates.name.spineBandScript': 'Bant ve el yazısı',
|
||||
'studio.templates.name.posterPopSunburst': 'Işınlı poster',
|
||||
'studio.templates.name.posterPopSplit': 'Bölünmüş poster',
|
||||
'studio.templates.name.posterPopStack': 'Katmanlı poster',
|
||||
'studio.templates.name.cutoutMatisse': 'Kâğıt kesikler',
|
||||
'studio.templates.name.cutoutArch': 'Kesik kemer',
|
||||
'studio.templates.name.cutoutCollage': 'Kesik kâğıt kolajı',
|
||||
'studio.templates.name.postcardHouses': 'Şehirli kartpostal',
|
||||
'studio.templates.name.postcardCoast': 'Kıyılı kartpostal',
|
||||
'studio.templates.name.postcardPeaks': 'Dağlı kartpostal',
|
||||
'studio.templates.name.gridBadgeCircle': 'Daireli ızgara',
|
||||
'studio.templates.name.gridBadgeStamp': 'Damgalı ızgara',
|
||||
'studio.templates.name.gridBadgeSeal': 'Mühürlü ızgara',
|
||||
'studio.templates.name.minimalSerifClassic': 'Klasik tırnaklı',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Dikey tırnaklı',
|
||||
'studio.templates.name.minimalSerifWindow': 'Pencereli tırnaklı',
|
||||
'studio.templates.name.stampCollageChecker': 'Damalı kolaj',
|
||||
'studio.templates.name.stampCollageRow': 'Pul sırası',
|
||||
'studio.templates.name.stampCollageArches': 'Dama üstünde kemerler',
|
||||
'studio.templates.name.heroScriptWord': 'Fotoğraf ve el yazısı',
|
||||
'studio.templates.name.heroTrioWordart': 'Fotoğraflı kelime ve üç fotoğraf',
|
||||
'studio.templates.name.storyQuote': 'Öykü ve alıntı',
|
||||
'studio.templates.name.bleedBandCaption': 'Bantlı panorama',
|
||||
'studio.templates.name.twoToneSplit': 'İki ton',
|
||||
'studio.templates.name.dayOpener': 'Günün açılışı',
|
||||
'studio.templates.name.polaroidStack': 'Polaroidler',
|
||||
'studio.templates.name.stampRow': 'Yan yana pullar',
|
||||
'studio.templates.name.archGallery': 'Kemer galerisi',
|
||||
'studio.templates.name.mapTickets': 'Harita ve biletler',
|
||||
'studio.templates.name.filmstripScript': 'Film şeridi',
|
||||
'studio.templates.name.postcardBack': 'Kartpostal arkası',
|
||||
'studio.templates.name.gridTight': 'Sık ızgara',
|
||||
'studio.templates.name.gridFeature': 'Öne çıkan ızgara',
|
||||
'studio.templates.name.gridColumns': 'Sütunlu ızgara',
|
||||
'studio.templates.name.gridMosaic': 'Mozaik ızgara',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Оформлені сторінки',
|
||||
'studio.templates.style': 'Стиль',
|
||||
'studio.templates.all': 'Усі',
|
||||
'studio.templates.hint':
|
||||
'Кожен макет показано з фотографіями й текстами цієї сторінки. Клацання верстає сторінку за ним, а те, чому забракло місця, лишається при сторінці.',
|
||||
'studio.templates.archetype.spine-band': 'Смуга біля корінця',
|
||||
'studio.templates.archetype.poster-pop': 'Плакат',
|
||||
'studio.templates.archetype.cutout': 'Аплікація',
|
||||
'studio.templates.archetype.postcard': 'Листівка',
|
||||
'studio.templates.archetype.grid-badge': 'Сітка й значок',
|
||||
'studio.templates.archetype.minimal-serif': 'Мінімалізм',
|
||||
'studio.templates.archetype.stamp-collage': 'Марки',
|
||||
'studio.templates.name.spineBandStitched': 'Прошита смуга',
|
||||
'studio.templates.name.spineBandGingham': 'Смуга в клітинку',
|
||||
'studio.templates.name.spineBandScript': 'Смуга й рукописний шрифт',
|
||||
'studio.templates.name.posterPopSunburst': 'Плакат із променями',
|
||||
'studio.templates.name.posterPopSplit': 'Розділений плакат',
|
||||
'studio.templates.name.posterPopStack': 'Багатоярусний плакат',
|
||||
'studio.templates.name.cutoutMatisse': 'Паперова аплікація',
|
||||
'studio.templates.name.cutoutArch': 'Вирізана арка',
|
||||
'studio.templates.name.cutoutCollage': 'Колаж із вирізок',
|
||||
'studio.templates.name.postcardHouses': 'Листівка з містом',
|
||||
'studio.templates.name.postcardCoast': 'Листівка з узбережжям',
|
||||
'studio.templates.name.postcardPeaks': 'Листівка з горами',
|
||||
'studio.templates.name.gridBadgeCircle': 'Сітка з колом',
|
||||
'studio.templates.name.gridBadgeStamp': 'Сітка зі штампом',
|
||||
'studio.templates.name.gridBadgeSeal': 'Сітка з печаткою',
|
||||
'studio.templates.name.minimalSerifClassic': 'Класична антиква',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Вертикальна антиква',
|
||||
'studio.templates.name.minimalSerifWindow': 'Антиква з вікном',
|
||||
'studio.templates.name.stampCollageChecker': 'Колаж у клітинку',
|
||||
'studio.templates.name.stampCollageRow': 'Ряд марок',
|
||||
'studio.templates.name.stampCollageArches': 'Арки на клітинці',
|
||||
'studio.templates.name.heroScriptWord': 'Фото й рукописний шрифт',
|
||||
'studio.templates.name.heroTrioWordart': 'Слово з фото й три фото',
|
||||
'studio.templates.name.storyQuote': 'Розповідь із цитатою',
|
||||
'studio.templates.name.bleedBandCaption': 'Панорама зі смугою',
|
||||
'studio.templates.name.twoToneSplit': 'Два кольори',
|
||||
'studio.templates.name.dayOpener': 'Початок дня',
|
||||
'studio.templates.name.polaroidStack': 'Полароїди',
|
||||
'studio.templates.name.stampRow': 'Марки в ряд',
|
||||
'studio.templates.name.archGallery': 'Галерея арок',
|
||||
'studio.templates.name.mapTickets': 'Мапа й квитки',
|
||||
'studio.templates.name.filmstripScript': 'Кіноплівка',
|
||||
'studio.templates.name.postcardBack': 'Зворот листівки',
|
||||
'studio.templates.name.gridTight': 'Щільна сітка',
|
||||
'studio.templates.name.gridFeature': 'Сітка з головним фото',
|
||||
'studio.templates.name.gridColumns': 'Сітка колонками',
|
||||
'studio.templates.name.gridMosaic': 'Мозаїчна сітка',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': 'Trang thiết kế sẵn',
|
||||
'studio.templates.style': 'Phong cách',
|
||||
'studio.templates.all': 'Tất cả',
|
||||
'studio.templates.hint':
|
||||
'Mỗi mẫu được vẽ bằng ảnh và chữ của trang này. Một cú nhấp sẽ dàn trang theo mẫu đó, và phần nào không vừa sẽ được giữ lại cùng trang.',
|
||||
'studio.templates.archetype.spine-band': 'Dải gáy sách',
|
||||
'studio.templates.archetype.poster-pop': 'Áp phích',
|
||||
'studio.templates.archetype.cutout': 'Cắt giấy',
|
||||
'studio.templates.archetype.postcard': 'Bưu thiếp',
|
||||
'studio.templates.archetype.grid-badge': 'Lưới và huy hiệu',
|
||||
'studio.templates.archetype.minimal-serif': 'Tối giản',
|
||||
'studio.templates.archetype.stamp-collage': 'Tem thư',
|
||||
'studio.templates.name.spineBandStitched': 'Dải khâu chỉ',
|
||||
'studio.templates.name.spineBandGingham': 'Dải kẻ ô',
|
||||
'studio.templates.name.spineBandScript': 'Dải và chữ viết tay',
|
||||
'studio.templates.name.posterPopSunburst': 'Áp phích tia nắng',
|
||||
'studio.templates.name.posterPopSplit': 'Áp phích chia đôi',
|
||||
'studio.templates.name.posterPopStack': 'Áp phích xếp tầng',
|
||||
'studio.templates.name.cutoutMatisse': 'Giấy cắt dán',
|
||||
'studio.templates.name.cutoutArch': 'Vòm cắt giấy',
|
||||
'studio.templates.name.cutoutCollage': 'Ảnh ghép cắt giấy',
|
||||
'studio.templates.name.postcardHouses': 'Bưu thiếp phố',
|
||||
'studio.templates.name.postcardCoast': 'Bưu thiếp biển',
|
||||
'studio.templates.name.postcardPeaks': 'Bưu thiếp núi',
|
||||
'studio.templates.name.gridBadgeCircle': 'Lưới có vòng tròn',
|
||||
'studio.templates.name.gridBadgeStamp': 'Lưới có con dấu',
|
||||
'studio.templates.name.gridBadgeSeal': 'Lưới có dấu niêm',
|
||||
'studio.templates.name.minimalSerifClassic': 'Chữ có chân cổ điển',
|
||||
'studio.templates.name.minimalSerifPortrait': 'Chữ có chân dọc',
|
||||
'studio.templates.name.minimalSerifWindow': 'Chữ có chân và khung',
|
||||
'studio.templates.name.stampCollageChecker': 'Ảnh ghép bàn cờ',
|
||||
'studio.templates.name.stampCollageRow': 'Hàng tem',
|
||||
'studio.templates.name.stampCollageArches': 'Vòm trên bàn cờ',
|
||||
'studio.templates.name.heroScriptWord': 'Ảnh và chữ viết tay',
|
||||
'studio.templates.name.heroTrioWordart': 'Chữ lồng ảnh và ba ảnh',
|
||||
'studio.templates.name.storyQuote': 'Câu chuyện và trích dẫn',
|
||||
'studio.templates.name.bleedBandCaption': 'Toàn cảnh có dải',
|
||||
'studio.templates.name.twoToneSplit': 'Hai tông màu',
|
||||
'studio.templates.name.dayOpener': 'Mở đầu ngày',
|
||||
'studio.templates.name.polaroidStack': 'Ảnh Polaroid',
|
||||
'studio.templates.name.stampRow': 'Tem xếp hàng',
|
||||
'studio.templates.name.archGallery': 'Phòng tranh vòm',
|
||||
'studio.templates.name.mapTickets': 'Bản đồ và vé',
|
||||
'studio.templates.name.filmstripScript': 'Dải phim',
|
||||
'studio.templates.name.postcardBack': 'Mặt sau bưu thiếp',
|
||||
'studio.templates.name.gridTight': 'Lưới khít',
|
||||
'studio.templates.name.gridFeature': 'Lưới ảnh chính',
|
||||
'studio.templates.name.gridColumns': 'Lưới cột',
|
||||
'studio.templates.name.gridMosaic': 'Lưới khảm',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': '設計好的頁面',
|
||||
'studio.templates.style': '風格',
|
||||
'studio.templates.all': '全部',
|
||||
'studio.templates.hint': '每個設計都以本頁的照片和文字呈現。點一下即可依此設計排版本頁,放不下的內容會保留在本頁。',
|
||||
'studio.templates.archetype.spine-band': '書背色帶',
|
||||
'studio.templates.archetype.poster-pop': '海報',
|
||||
'studio.templates.archetype.cutout': '剪紙',
|
||||
'studio.templates.archetype.postcard': '明信片',
|
||||
'studio.templates.archetype.grid-badge': '網格與徽章',
|
||||
'studio.templates.archetype.minimal-serif': '極簡',
|
||||
'studio.templates.archetype.stamp-collage': '郵票',
|
||||
'studio.templates.name.spineBandStitched': '縫線色帶',
|
||||
'studio.templates.name.spineBandGingham': '格紋色帶',
|
||||
'studio.templates.name.spineBandScript': '色帶與手寫字',
|
||||
'studio.templates.name.posterPopSunburst': '放射線海報',
|
||||
'studio.templates.name.posterPopSplit': '分割海報',
|
||||
'studio.templates.name.posterPopStack': '疊層海報',
|
||||
'studio.templates.name.cutoutMatisse': '紙片剪影',
|
||||
'studio.templates.name.cutoutArch': '剪紙拱門',
|
||||
'studio.templates.name.cutoutCollage': '剪紙拼貼',
|
||||
'studio.templates.name.postcardHouses': '城市明信片',
|
||||
'studio.templates.name.postcardCoast': '海岸明信片',
|
||||
'studio.templates.name.postcardPeaks': '山景明信片',
|
||||
'studio.templates.name.gridBadgeCircle': '有圓形的網格',
|
||||
'studio.templates.name.gridBadgeStamp': '有印章的網格',
|
||||
'studio.templates.name.gridBadgeSeal': '有封印的網格',
|
||||
'studio.templates.name.minimalSerifClassic': '經典襯線',
|
||||
'studio.templates.name.minimalSerifPortrait': '直式襯線',
|
||||
'studio.templates.name.minimalSerifWindow': '有窗框的襯線',
|
||||
'studio.templates.name.stampCollageChecker': '棋盤格拼貼',
|
||||
'studio.templates.name.stampCollageRow': '一排郵票',
|
||||
'studio.templates.name.stampCollageArches': '棋盤格上的拱門',
|
||||
'studio.templates.name.heroScriptWord': '照片與手寫字',
|
||||
'studio.templates.name.heroTrioWordart': '照片文字與三張照片',
|
||||
'studio.templates.name.storyQuote': '故事與引言',
|
||||
'studio.templates.name.bleedBandCaption': '有色帶的全景',
|
||||
'studio.templates.name.twoToneSplit': '雙色',
|
||||
'studio.templates.name.dayOpener': '一天的開始',
|
||||
'studio.templates.name.polaroidStack': '拍立得',
|
||||
'studio.templates.name.stampRow': '並排郵票',
|
||||
'studio.templates.name.archGallery': '拱門藝廊',
|
||||
'studio.templates.name.mapTickets': '地圖與票根',
|
||||
'studio.templates.name.filmstripScript': '底片條',
|
||||
'studio.templates.name.postcardBack': '明信片背面',
|
||||
'studio.templates.name.gridTight': '緊密網格',
|
||||
'studio.templates.name.gridFeature': '主圖網格',
|
||||
'studio.templates.name.gridColumns': '分欄網格',
|
||||
'studio.templates.name.gridMosaic': '馬賽克網格',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -65,6 +65,7 @@ import studioPaint from './studioPaint';
|
||||
import studioPhoto from './studioPhoto';
|
||||
import studioShapes from './studioShapes';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioTemplates from './studioTemplates';
|
||||
import studioText from './studioText';
|
||||
import studioThemes from './studioThemes';
|
||||
import system_notice from './system_notice';
|
||||
@@ -162,5 +163,6 @@ const locale = {
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
...studioTemplates,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const studioTemplates: TranslationStrings = {
|
||||
'studio.templates.decorated': '设计好的页面',
|
||||
'studio.templates.style': '风格',
|
||||
'studio.templates.all': '全部',
|
||||
'studio.templates.hint': '每个设计都用本页的照片和文字展示。点击即可按此设计排版本页,放不下的内容会保留在本页。',
|
||||
'studio.templates.archetype.spine-band': '书脊色带',
|
||||
'studio.templates.archetype.poster-pop': '海报',
|
||||
'studio.templates.archetype.cutout': '剪纸',
|
||||
'studio.templates.archetype.postcard': '明信片',
|
||||
'studio.templates.archetype.grid-badge': '网格与徽章',
|
||||
'studio.templates.archetype.minimal-serif': '极简',
|
||||
'studio.templates.archetype.stamp-collage': '邮票',
|
||||
'studio.templates.name.spineBandStitched': '缝线色带',
|
||||
'studio.templates.name.spineBandGingham': '格纹色带',
|
||||
'studio.templates.name.spineBandScript': '色带与手写体',
|
||||
'studio.templates.name.posterPopSunburst': '放射线海报',
|
||||
'studio.templates.name.posterPopSplit': '分割海报',
|
||||
'studio.templates.name.posterPopStack': '叠层海报',
|
||||
'studio.templates.name.cutoutMatisse': '纸片剪影',
|
||||
'studio.templates.name.cutoutArch': '剪纸拱门',
|
||||
'studio.templates.name.cutoutCollage': '剪纸拼贴',
|
||||
'studio.templates.name.postcardHouses': '城市明信片',
|
||||
'studio.templates.name.postcardCoast': '海岸明信片',
|
||||
'studio.templates.name.postcardPeaks': '山景明信片',
|
||||
'studio.templates.name.gridBadgeCircle': '带圆形的网格',
|
||||
'studio.templates.name.gridBadgeStamp': '带印章的网格',
|
||||
'studio.templates.name.gridBadgeSeal': '带封印的网格',
|
||||
'studio.templates.name.minimalSerifClassic': '经典衬线',
|
||||
'studio.templates.name.minimalSerifPortrait': '竖版衬线',
|
||||
'studio.templates.name.minimalSerifWindow': '带窗口的衬线',
|
||||
'studio.templates.name.stampCollageChecker': '棋盘格拼贴',
|
||||
'studio.templates.name.stampCollageRow': '一排邮票',
|
||||
'studio.templates.name.stampCollageArches': '棋盘格上的拱门',
|
||||
'studio.templates.name.heroScriptWord': '照片与手写体',
|
||||
'studio.templates.name.heroTrioWordart': '照片文字与三张照片',
|
||||
'studio.templates.name.storyQuote': '故事与引言',
|
||||
'studio.templates.name.bleedBandCaption': '带色带的全景',
|
||||
'studio.templates.name.twoToneSplit': '双色',
|
||||
'studio.templates.name.dayOpener': '一天的开始',
|
||||
'studio.templates.name.polaroidStack': '拍立得',
|
||||
'studio.templates.name.stampRow': '并排邮票',
|
||||
'studio.templates.name.archGallery': '拱门画廊',
|
||||
'studio.templates.name.mapTickets': '地图与票根',
|
||||
'studio.templates.name.filmstripScript': '胶片条',
|
||||
'studio.templates.name.postcardBack': '明信片背面',
|
||||
'studio.templates.name.gridTight': '紧密网格',
|
||||
'studio.templates.name.gridFeature': '主图网格',
|
||||
'studio.templates.name.gridColumns': '分栏网格',
|
||||
'studio.templates.name.gridMosaic': '马赛克网格',
|
||||
};
|
||||
|
||||
export default studioTemplates;
|
||||
@@ -204,6 +204,11 @@ export * from './book/theme/book-theme-links';
|
||||
export * from './book/theme/book-theme-ops';
|
||||
export * from './book/theme/book-theme-photos';
|
||||
export { bookThemeCatalogue, bookThemeLine } from './book/theme/book-theme-describe';
|
||||
export * from './book/templates/template-format';
|
||||
export * from './book/templates/template-catalogue';
|
||||
export * from './book/templates/template-apply';
|
||||
export * from './book/templates/template-words';
|
||||
export * from './book/templates/template-extract';
|
||||
export * from './share/share.schema';
|
||||
export * from './storage/storage.schema';
|
||||
export * from './settings/settings.schema';
|
||||
|
||||
@@ -77,7 +77,11 @@ sonar.coverage.exclusions=\
|
||||
# shared/src/book/stickers/data/ is the Studio's sticker library: one file per theme
|
||||
# of drawings as SVG path strings with their tags and colour slots, whose
|
||||
# repeated object shape is a data layout, not copied logic.
|
||||
sonar.cpd.exclusions=shared/src/i18n/**,server/src/db/kysely/**,shared/src/book/book-icon-names-*.data.ts,shared/src/book/stickers/data/**
|
||||
# shared/src/book/templates/data/ is the decorated templates: whole pages as
|
||||
# full document elements in fractions, as the Studio draws and the extract
|
||||
# script writes them, so every element repeats the fields of its kind. That
|
||||
# repetition is the format of the data, not copied logic.
|
||||
sonar.cpd.exclusions=shared/src/i18n/**,server/src/db/kysely/**,shared/src/book/book-icon-names-*.data.ts,shared/src/book/stickers/data/**,shared/src/book/templates/data/**
|
||||
|
||||
# The full analysis of a push to dev outgrows the JS/TS analyser's default heap
|
||||
# of about 4 GB since the ORM migration and stops with "Analysis of JS/TS files
|
||||
|
||||
@@ -218,13 +218,13 @@ Requires `journey:read` or `journey:write` scope.
|
||||
| `delete_journey_recorded_day` | Drop the recorded route a journey keeps for one day. Entries and texts stay, and Dawarich keeps its own recording. Needs edit access. |
|
||||
| `get_book` | The journey's photo book from the Studio, without its document: version, title, page format and every page with its id, role, photos and first words. Page and element ids for the tools below come from here. Any member may read it. |
|
||||
| `describe_book_page` | One page of the book in words: every element with its id, frame in millimetres and photo or text, then the findings (overlaps, content in the fold or past the trim, empty frames, low print resolution, words past their frame). |
|
||||
| `list_book_layouts` | The layouts a page can take: templates for double spreads and single pages, photo grids, and the book's own saved layouts, with what each does to what is on the page. |
|
||||
| `list_book_layouts` | The layouts a page can take: templates for double spreads and single pages, the decorated templates (whole designed pages, each with its style, tags, photo frames and the roles its words go into), photo grids, and the book's own saved layouts, with what each does to what is on the page. |
|
||||
| `list_book_assets` | Search the Studio's sticker library: illustrations of landmarks, travel things, plants, food, paper and tape, doodles and ornaments, each with its id, a one-line description, its tags, its proportions and its colour slots. `edit_book_page` places one with `add` and recolours it with `style`. |
|
||||
| `create_book` | Lay down a blank book (cover, first page, one double spread, last page, back) for a journey that has none, in a page format of your choice. Needs edit access. |
|
||||
| `add_book_pages` | Add up to 10 empty double spreads, always between the first and the last page, optionally on a template or tied to an entry. Needs edit access. |
|
||||
| `move_book_page` | Move one double spread to another place between the first and the last page; the answer says where it stood. A move to where the page already is changes nothing. Needs edit access. |
|
||||
| `delete_book_pages` | Delete up to 10 double spreads. The cover, first, last and back pages stay. Needs edit access. |
|
||||
| `edit_book_page` | Change one page with up to 20 operations, all or nothing: a layout, a gallery photo into a frame, its crop and look, swapping photos, texts and captions, moving and resizing, removing, the page colour. A new element is added by what it is, and its look follows with `style` by the id `add` gave it, in the same call. Locked elements are left alone, and photos come only from the journey's gallery. Needs edit access. |
|
||||
| `edit_book_page` | Change one page with up to 20 operations, all or nothing: a layout (a built-in or decorated template, a grid or a saved layout), a gallery photo into a frame, its crop and look, swapping photos, texts and captions, moving and resizing, removing, the page colour. A new element is added by what it is, and its look follows with `style` by the id `add` gave it, in the same call. Locked elements are left alone, and photos come only from the journey's gallery. Needs edit access. |
|
||||
| `set_book_cover` | The cover's layout, photo, title and subtitle. Needs edit access. |
|
||||
| `suggest_book_photos` | Photos of the journey's gallery for a page or the cover, grouped by day and place, best first: near duplicates (bursts and near-identical pictures) are folded onto the sharpest, and possibly blurry or low-resolution photos are named rather than picked. Narrow it to one entry or day. Sharpness and likeness are measured once per photo in the background; it cannot judge faces or what a photo shows. Any member may use it. |
|
||||
| `restore_book_page` | Put the pages an earlier change answered under `previous_spreads` back as they were, all in one call, only while the book is still at the version that change answered. Photos must be in the gallery or the book, and locked elements stay as they are. Meant for undo. Needs edit access. |
|
||||
|
||||
Reference in New Issue
Block a user