Initial commit

This commit is contained in:
Z User
2026-06-06 05:21:10 +00:00
Unverified
commit 6664758a6d
493 changed files with 135653 additions and 0 deletions

View File

@@ -0,0 +1,538 @@
## Geometric Decoration System — Pure docx-js Decorations
### Design Philosophy
Uses only docx-js native capabilities for visual decoration — no external tools (like Playwright screenshots). Suitable for covers, chapter separators, page background enhancement.
**When to fall back to Playwright?**
Only when gradients, complex illustrations, or brand visuals are needed that pure OOXML cannot express. Default: prefer native solutions below.
### Decoration Element Library
#### 1. Color Strip — Table Simulation
Single-row single-column borderless table + background color to create horizontal color strips.
```js
function colorStrip(color, height = 80) {
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
borders: { top: NB, bottom: NB, left: NB, right: NB,
insideHorizontal: NB, insideVertical: NB },
rows: [new TableRow({
height: { value: height, rule: "exact" },
children: [new TableCell({
shading: { type: ShadingType.CLEAR, fill: color.replace("#", "") },
borders: { top: NB, bottom: NB, left: NB, right: NB },
children: [new Paragraph({ children: [] })],
})],
})],
});
}
// ══════════════════════════════════════════════════════════════
// R6 — Editorial Warm (minimal, warm white bg, no decorations)
// ══════════════════════════════════════════════════════════════
// Suitable for: lesson plans (non-STEM), cultural/creative, newsletters,
// event planning, internal reports, light-weight documents
// NOT for: formal business, consulting, finance, government, academic
// Title constraint: single line only (≤20 chars). Longer titles → route to R1.
//
// Structure: 2-row wrapper table (no border, warm bg shading)
// Row 1 (content): category → title → subtitle → fields
// Row 2 (footer): left English title + right label
// All spacing via paragraph indent (WPS safe, no cell margins).
function buildCoverR6(config) {
const P = config.palette;
const PAD_L = 1300, PAD_R = 1100;
const ind = { left: PAD_L, right: PAD_R };
const FOOTER_H = 900;
const CONTENT_H = 16838 - FOOTER_H;
const shading = { fill: P.bg || "F7F7F5", type: ShadingType.CLEAR };
// ⚠️ R6 uses a simplified title layout: prefer single line, shrink font to fit
const availW = 11906 - PAD_L - PAD_R;
const { titlePt, titleLines } = calcTitleLayoutR6(config.title, availW, 36, 22);
const titleSize = titlePt * 2;
const lineH = Math.ceil(titlePt * 23 * 1.3);
// Dynamic top spacing
const titleH = titleLines.length * (titleSize * 10 + 200);
const categoryH = 22 * 10 + 900;
const subtitleH = config.subtitle ? (28 * 10 + 1200) : 0;
const fieldsH = (config.metaLines || []).length * (24 * 10 + 100);
const contentH = categoryH + titleH + subtitleH + fieldsH;
const remaining = Math.max(CONTENT_H - 1200 - contentH, 400);
const topSpacing = Math.floor(remaining * 0.55);
const children = [];
// 1. Top spacer (dynamic)
children.push(new Paragraph({ indent: ind, spacing: { before: topSpacing } }));
// 2. Category label (small, wide letter-spacing)
if (config.englishLabel) {
children.push(new Paragraph({
indent: ind, spacing: { after: 900 },
children: [new TextRun({
text: config.englishLabel, size: 22,
color: P.cover.metaColor || "9A9A9A",
font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" },
characterSpacing: 60,
})],
}));
}
// 3. Title (single line preferred, dynamic font size)
for (let i = 0; i < titleLines.length; i++) {
children.push(new Paragraph({
indent: ind,
spacing: { after: i < titleLines.length - 1 ? 60 : 300, line: lineH, lineRule: "atLeast" },
children: [new TextRun({
text: titleLines[i], size: titleSize,
color: P.cover.titleColor || "2C2C2C",
font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" },
characterSpacing: 30,
})],
}));
}
// 4. Subtitle
if (config.subtitle) {
children.push(new Paragraph({
indent: ind, spacing: { after: 1200 },
children: [new TextRun({
text: config.subtitle, size: 28,
color: P.cover.subtitleColor || "6B6B6B",
font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" },
characterSpacing: 15,
})],
}));
}
// 5. Meta fields (tab-aligned label + value)
for (const line of (config.metaLines || [])) {
// Expect "labelvalue" format or plain text
const sep = line.indexOf("") !== -1 ? "" : (line.indexOf(":") !== -1 ? ":" : null);
const label = sep ? line.split(sep)[0].trim() : line;
const value = sep ? line.split(sep).slice(1).join(sep).trim() : "";
children.push(new Paragraph({
indent: ind, spacing: { after: 100 },
tabStops: [{ type: TabStopType.LEFT, position: PAD_L + 1600 }],
children: [
new TextRun({ text: label, size: 22, color: P.cover.metaColor || "9A9A9A",
font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" }, characterSpacing: 20 }),
...(value ? [
new TextRun({ text: "\t" }),
new TextRun({ text: value, size: 24, color: P.cover.subtitleColor || "6B6B6B",
font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" }, characterSpacing: 8 }),
] : []),
],
}));
}
// 6. Footer (2-column borderless table)
const footerLeft = config.footerLeft || "";
const footerRight = config.footerRight || "";
// Adaptive font size for long English footer text
const flSize = footerLeft.length > 60 ? 14 : (footerLeft.length > 40 ? 16 : 18);
const flSpacing = footerLeft.length > 60 ? 5 : (footerLeft.length > 40 ? 10 : 20);
const footerTable = new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
layout: TableLayoutType.FIXED, borders: allNoBorders,
rows: [new TableRow({
children: [
new TableCell({
width: { size: 70, type: WidthType.PERCENTAGE }, borders: noBorders, shading,
children: [new Paragraph({
indent: { left: PAD_L },
children: [new TextRun({ text: footerLeft, size: flSize,
color: P.cover.footerColor || "9A9A9A",
font: { ascii: "Calibri" }, characterSpacing: flSpacing })],
})],
}),
new TableCell({
width: { size: 30, type: WidthType.PERCENTAGE }, borders: noBorders, shading,
children: [new Paragraph({
alignment: AlignmentType.RIGHT, indent: { right: PAD_R },
children: [new TextRun({ text: footerRight, size: 18,
color: P.cover.footerColor || "9A9A9A",
font: { ascii: "Calibri" }, characterSpacing: 20 })],
})],
}),
],
})],
});
// 7. 2-row wrapper (content + footer)
return [new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
layout: TableLayoutType.FIXED, borders: allNoBorders,
rows: [
new TableRow({
height: { value: CONTENT_H, rule: "exact" },
children: [new TableCell({
shading, borders: noBorders,
margins: { top: 0, bottom: 0, left: 0, right: 0 },
verticalAlign: VerticalAlign.TOP,
children,
})],
}),
new TableRow({
height: { value: FOOTER_H, rule: "exact" },
children: [new TableCell({
shading, borders: noBorders,
margins: { top: 0, bottom: 0, left: 0, right: 0 },
verticalAlign: VerticalAlign.CENTER,
children: [footerTable],
})],
}),
],
})];
}
// R6 title layout: prefer FEWER lines over larger font size (single line best)
function calcTitleLayoutR6(title, availableWidthTw, preferredPt, minPt) {
const step = 2;
// Try to fit in 1 line (shrink font if needed)
for (let pt = preferredPt; pt >= minPt; pt -= step) {
const charWidthTw = pt * 23 * 0.5; // CJK ~50% em width
const charsPerLine = Math.floor(availableWidthTw / charWidthTw);
if (title.length <= charsPerLine) return { titlePt: pt, titleLines: [title] };
}
// Can't fit in 1 line, try 2 lines at largest possible font
for (let pt = preferredPt; pt >= minPt; pt -= step) {
const charWidthTw = pt * 23 * 0.5;
const charsPerLine = Math.floor(availableWidthTw / charWidthTw);
const lines = splitTitleLines(title, charsPerLine);
if (lines.length <= 2) return { titlePt: pt, titleLines: lines };
}
// Fallback: minPt, up to 3 lines
const charWidthTw = minPt * 23 * 0.5;
const charsPerLine = Math.floor(availableWidthTw / charWidthTw);
return { titlePt: minPt, titleLines: splitTitleLines(title, charsPerLine) };
}
// Usage: cover top decoration
// children: [colorStrip(P.accent, 120), ...]
```
#### 2. Side Ribbon
Uses left border to create vertical ribbon effect.
```js
function sideRibbon(content, color, width = 14) {
return new Paragraph({
border: {
left: { style: BorderStyle.SINGLE, size: width, color: color.replace("#", ""), space: 12 },
},
indent: { left: 240 },
spacing: { before: 100, after: 100 },
children: content,
});
}
// Usage: emphasis quotes, chapter tips
// sideRibbon([new TextRun({ text: "Key Insight", bold: true })], P.accent)
```
#### 3. Border Compositions
```js
// Top thick line + bottom thin line — title area frame
function frameTitle(titleRuns) {
return new Paragraph({
border: {
top: { style: BorderStyle.SINGLE, size: 18, color: c(P.accent) },
bottom: { style: BorderStyle.SINGLE, size: 4, color: c(P.accent) },
},
spacing: { before: 400, after: 200 },
alignment: AlignmentType.CENTER,
children: titleRuns,
});
}
// L-shape border — left + bottom
function lShapeBorder(content) {
return new Paragraph({
border: {
left: { style: BorderStyle.SINGLE, size: 12, color: c(P.accent), space: 10 },
bottom: { style: BorderStyle.SINGLE, size: 12, color: c(P.accent) },
},
indent: { left: 300 },
spacing: { before: 200, after: 300 },
children: content,
});
}
// Double-line frame — top and bottom double lines
function doubleLine(content) {
return new Paragraph({
border: {
top: { style: BorderStyle.DOUBLE, size: 6, color: c(P.accent) },
bottom: { style: BorderStyle.DOUBLE, size: 6, color: c(P.accent) },
},
spacing: { before: 200, after: 200 },
alignment: AlignmentType.CENTER,
children: content,
});
}
```
#### 4. Gradient Simulation
Multiple narrow color strips to simulate gradient effect.
```js
function gradientStrip(startColor, endColor, steps = 5, totalHeight = 200) {
const rows = [];
const h = Math.floor(totalHeight / steps);
for (let i = 0; i < steps; i++) {
const ratio = i / (steps - 1);
const blended = blendColors(startColor, endColor, ratio);
rows.push(new TableRow({
height: { value: h, rule: "exact" },
children: [new TableCell({
shading: { type: ShadingType.CLEAR, fill: blended },
borders: { top: NB, bottom: NB, left: NB, right: NB },
children: [new Paragraph({ children: [] })],
})],
}));
}
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
borders: { top: NB, bottom: NB, left: NB, right: NB,
insideHorizontal: NB, insideVertical: NB },
rows,
});
}
function blendColors(hex1, hex2, ratio) {
const r1 = parseInt(hex1.slice(1, 3), 16), g1 = parseInt(hex1.slice(3, 5), 16), b1 = parseInt(hex1.slice(5, 7), 16);
const r2 = parseInt(hex2.slice(1, 3), 16), g2 = parseInt(hex2.slice(3, 5), 16), b2 = parseInt(hex2.slice(5, 7), 16);
const r = Math.round(r1 + (r2 - r1) * ratio), g = Math.round(g1 + (g2 - g1) * ratio), b = Math.round(b1 + (b2 - b1) * ratio);
return `${r.toString(16).padStart(2,"0")}${g.toString(16).padStart(2,"0")}${b.toString(16).padStart(2,"0")}`;
}
```
#### 5. Symbol Ornaments
```js
// Section divider line — for chapter separation
function ornamentDivider(symbol = "◆", count = 3) {
const ornament = Array(count).fill(symbol).join(" ");
return new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { before: 400, after: 400 },
children: [new TextRun({ text: ornament, size: 20, color: c(P.accent) })],
});
}
// Common decoration symbols
// ◆ ◇ ● ○ ★ ☆ ■ □ ▲ △ ─ ━ ═ ║ ╔ ╗ ╚ ╝
// Ornamental: ❧ ❦ ✦ ✧ ✿ ❀ ❁ ※
```
#### 6. Info Card — Table Implementation
```js
function infoCard(title, items, accentColor) {
const ac = accentColor.replace("#", "");
const headerRow = new TableRow({
children: [new TableCell({
columnSpan: 2,
shading: { type: ShadingType.CLEAR, fill: ac },
margins: { top: 80, bottom: 80, left: 160, right: 160 },
borders: { top: NB, bottom: NB, left: NB, right: NB },
children: [new Paragraph({
children: [new TextRun({ text: title, bold: true, size: 24, color: "FFFFFF" })],
})],
})],
});
const dataRows = items.map(([label, value]) => new TableRow({
children: [
new TableCell({
width: { size: 30, type: WidthType.PERCENTAGE },
margins: { top: 60, bottom: 60, left: 160, right: 80 },
shading: { type: ShadingType.CLEAR, fill: "F8F9FA" },
borders: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "E0E0E0" },
top: NB, left: NB, right: NB },
children: [new Paragraph({ children: [new TextRun({ text: label, size: 21, color: "666666" })] })],
}),
new TableCell({
margins: { top: 60, bottom: 60, left: 80, right: 160 },
borders: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "E0E0E0" },
top: NB, left: NB, right: NB },
children: [new Paragraph({ children: [new TextRun({ text: value, size: 21 })] })],
}),
],
}));
return new Table({
width: { size: 80, type: WidthType.PERCENTAGE },
alignment: AlignmentType.CENTER,
borders: { top: NB, bottom: NB, left: NB, right: NB,
insideHorizontal: NB, insideVertical: NB },
rows: [headerRow, ...dataRows],
});
}
```
// R7 — Swiss Tech Minimalist (slate grey bg, Klein blue accent, asymmetric layout)
// Suitable for: cultural/creative research, trend reports, brand strategy, design deliverables
// Palette: ST-1 (exclusive)
// Layout: left-aligned title (upper 20%), right-shifted subtitle with top rule,
// right-aligned info block with accent right border, Swiss cross anchor
// Key features: ■ square accent dot, open-frame tables, large whitespace
//
// ⚠️ MANDATORY: All cover non-negotiables apply (margin=0, 16838 exact, allNoBorders)
// ⚠️ Title uses calcTitleLayout() with maxPt=36 (not 40 — R7 uses lighter visual weight)
function buildCoverR7(config) {
const P = palettes[config.palette || "ST-1"];
const C = P.cover;
const padL = 600;
// Title layout — R7 uses 36pt max (lighter than R1-R4's 40pt)
const availW = 11906 - padL - 600;
const { titlePt, titleLines } = calcTitleLayout(config.title, availW, 36, 24);
const titleSize = titlePt * 2;
const lineH = Math.ceil(titlePt * 23);
// Dynamic spacing based on title lines
const topSpacer = titleLines.length <= 2 ? 1200 : 800;
const subtitleSpacer = titleLines.length <= 2 ? 1400 : 800;
const infoSpacer = titleLines.length <= 2 ? 2200 : 1200;
const children = [];
// 1. Swiss cross anchor — top-left decorative element
children.push(new Paragraph({
spacing: { before: 600 },
indent: { left: padL },
children: [new TextRun({
text: "\uFF0B", // fullwidth plus
size: 40, bold: true, color: C.titleColor,
font: { ascii: "Arial", eastAsia: "SimHei" },
})],
}));
// 2. Top spacer
children.push(new Paragraph({ spacing: { before: topSpacer } }));
// 3. Title lines — left-aligned, last line has accent ■
titleLines.forEach((line, i) => {
const isLast = i === titleLines.length - 1;
const runs = [new TextRun({
text: line, size: titleSize, color: C.titleColor,
font: { ascii: "Arial", eastAsia: "Noto Sans SC" },
})];
if (isLast) {
runs.push(new TextRun({
text: " \u25A0", // ■ black square
size: 24, color: P.accent,
font: { ascii: "Arial" },
}));
}
children.push(new Paragraph({
indent: { left: padL },
spacing: { after: isLast ? 200 : 80, line: lineH, lineRule: "atLeast" },
children: runs,
}));
});
// 4. Subtitle spacer
children.push(new Paragraph({ spacing: { before: subtitleSpacer } }));
// 5. Subtitle — right-shifted, top border rule, wide character spacing
if (config.subtitle) {
children.push(new Paragraph({
indent: { left: 3800, right: 600 },
border: { top: { style: BorderStyle.SINGLE, size: 2, color: C.titleColor, space: 14 } },
spacing: { after: 200 },
children: [new TextRun({
text: config.subtitle, size: 26, color: C.subtitleColor,
font: { ascii: "Arial", eastAsia: "Noto Sans SC" },
characterSpacing: 40,
})],
}));
}
// 6. Decorative horizontal line
children.push(new Paragraph({
spacing: { before: 600 },
border: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "C8D0DC", space: 0 } },
}));
// 7. Info spacer
children.push(new Paragraph({ spacing: { before: infoSpacer } }));
// 8. Info footer — right-aligned, 4 label+value pairs, accent right border
// Standard fields: ORGANIZATION, RESPONSIBILITY, REPORT NUMBER, DATE & EDITION
const metaEntries = config.metaEntries || [
{ label: "ORGANIZATION", value: config.organization || "" },
{ label: "RESPONSIBILITY", value: config.responsibility || "" },
{ label: "REPORT NUMBER", value: config.reportNumber || "" },
{ label: "DATE & EDITION", value: config.dateEdition || "" },
];
for (const entry of metaEntries) {
// Label — 7pt uppercase English
children.push(new Paragraph({
alignment: AlignmentType.RIGHT,
indent: { right: 800 },
border: { right: { style: BorderStyle.SINGLE, size: 12, color: P.accent, space: 16 } },
spacing: { after: 20 },
children: [new TextRun({
text: entry.label, size: 14, color: C.metaColor,
font: { ascii: "Arial" },
characterSpacing: 20,
})],
}));
// Value — 11pt bold
children.push(new Paragraph({
alignment: AlignmentType.RIGHT,
indent: { right: 800 },
border: { right: { style: BorderStyle.SINGLE, size: 12, color: P.accent, space: 16 } },
spacing: { after: 280 },
children: [new TextRun({
text: entry.value, size: 22, bold: true, color: C.titleColor,
font: { ascii: "Arial", eastAsia: "Noto Sans SC" },
})],
}));
}
// Wrap in 16838 exact wrapper table
return [new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
layout: TableLayoutType.FIXED,
borders: allNoBorders,
rows: [new TableRow({
height: { value: 16838, rule: "exact" },
children: [new TableCell({
shading: { type: ShadingType.CLEAR, fill: P.bg },
borders: noBorders,
verticalAlign: VerticalAlign.TOP,
children,
})],
})],
})];
}
### Decoration Usage Scenarios
| Scenario | Recommended Decoration | Combination |
|------|----------|----------|
| Report cover | Color strip + L-frame border | Top strip → Title area → L-frame author info |
| Proposal cover | Gradient simulation + double-line frame | Gradient bg → Double-line title |
| Chapter separator | Symbol ornament + side ribbon | Symbol divider → New chapter title with ribbon |
| Summary card | Info card | Standalone card displaying key metrics |
| Academic cover | Color strip + info table | Top strip → School name → Title → Info table |
---