@hirokisakabe/pom 8.7.0 → 8.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/dist/buildContext.js +4 -2
- package/dist/buildContext.js.map +1 -1
- package/dist/buildPptx.d.ts.map +1 -1
- package/dist/buildPptx.js +3 -1
- package/dist/buildPptx.js.map +1 -1
- package/dist/parseXml/coercionRules.js +12 -2
- package/dist/parseXml/coercionRules.js.map +1 -1
- package/dist/parseXml/parseXml.d.ts.map +1 -1
- package/dist/parseXml/parseXml.js +3 -3
- package/dist/parseXml/parseXml.js.map +1 -1
- package/dist/renderPptx/glowEffects.js +147 -0
- package/dist/renderPptx/glowEffects.js.map +1 -0
- package/dist/renderPptx/gradientFills.js +36 -8
- package/dist/renderPptx/gradientFills.js.map +1 -1
- package/dist/renderPptx/nodes/icon.js +13 -6
- package/dist/renderPptx/nodes/icon.js.map +1 -1
- package/dist/renderPptx/nodes/shape.js +24 -2
- package/dist/renderPptx/nodes/shape.js.map +1 -1
- package/dist/renderPptx/nodes/text.js +7 -2
- package/dist/renderPptx/nodes/text.js.map +1 -1
- package/dist/renderPptx/nodes/timeline.js +41 -14
- package/dist/renderPptx/nodes/timeline.js.map +1 -1
- package/dist/renderPptx/renderPptx.js +1 -1
- package/dist/renderPptx/units.js +8 -1
- package/dist/renderPptx/units.js.map +1 -1
- package/dist/renderPptx/utils/backgroundBorder.js +1 -1
- package/dist/shared/gradient.js +185 -19
- package/dist/shared/gradient.js.map +1 -1
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +15 -4
- package/dist/types.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gradientFills.js","names":[],"sources":["../../src/renderPptx/gradientFills.ts"],"sourcesContent":["/**\n * グラデーション塗りの実現\n *\n * pptxgenjs はグラデーション塗りを未サポート\n * (https://github.com/gitbrent/PptxGenJS/issues/102) のため、以下の方式で実現する:\n *\n * 1. レンダリング時: グラデーション指定のある shape をビルド内で一意なマーカー色の\n * 単色塗り (solidFill) として描画し、レジストリに登録する\n * 2. 出力時: pptx.write() / writeFile() をラップし、出力 zip 内のスライド XML の\n * `<a:solidFill><a:srgbClr val=\"マーカー色\"/></a:solidFill>` を\n * DrawingML ネイティブの `<a:gradFill>` に置換する\n *\n * 置換は pptxgenjs が生成する固定パターンに対する完全一致の文字列置換で行う。\n * スライド XML 全体をパーサーで往復させると無関係な要素の表現が変わり得るため、\n * 意図的に文字列置換を採用している。\n */\nimport type { LinearGradient } from \"../shared/gradient.ts\";\nimport { parseLinearGradient } from \"../shared/gradient.ts\";\n\ntype PptxGenJSInstance = import(\"pptxgenjs\").default;\ntype WriteProps = NonNullable<Parameters<PptxGenJSInstance[\"write\"]>[0]>;\ntype WriteFileProps = NonNullable<\n Parameters<PptxGenJSInstance[\"writeFile\"]>[0]\n>;\n\n// JSZip は CJS パッケージのため動的 import で読み込む\nasync function loadJSZip(): Promise<typeof import(\"jszip\")> {\n const mod = await import(\"jszip\");\n /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n return (mod as any).default ?? mod;\n /* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n}\n\ninterface RegisteredGradientFill {\n /** マーカー色 (6桁大文字 HEX、# なし) */\n marker: string;\n gradient: LinearGradient;\n /** 0-1。指定時は各カラーストップに alpha として反映する */\n opacity?: number;\n}\n\n/** マーカー色の探索開始値。reserveColors で予約済みの色はスキップされる */\nconst MARKER_BASE = 0x0f7a3d;\n\nexport class GradientFillRegistry {\n private readonly reserved = new Set<string>();\n private readonly markerBySpec = new Map<string, string>();\n private readonly registered: RegisteredGradientFill[] = [];\n private nextCandidate = MARKER_BASE;\n\n /**\n * テキスト中に現れる 6桁 HEX をマーカー候補から除外する。\n * 入力 XML 由来のユーザー指定色とマーカーの衝突を防ぐため、\n * register より前に入力 XML 文字列を渡しておく。\n */\n reserveColors(text: string): void {\n for (const match of text.matchAll(/[0-9a-fA-F]{6}/g)) {\n this.reserved.add(match[0].toUpperCase());\n }\n }\n\n /** グラデーションを登録し、対応するマーカー色を返す */\n register(gradient: LinearGradient, opacity?: number): string {\n const specKey = JSON.stringify({ gradient, opacity });\n const existing = this.markerBySpec.get(specKey);\n if (existing) return existing;\n\n let marker: string;\n do {\n marker = this.nextCandidate.toString(16).toUpperCase().padStart(6, \"0\");\n this.nextCandidate = (this.nextCandidate + 1) % 0x1000000;\n } while (this.reserved.has(marker));\n this.reserved.add(marker);\n\n this.markerBySpec.set(specKey, marker);\n this.registered.push({ marker, gradient, opacity });\n return marker;\n }\n\n get isEmpty(): boolean {\n return this.registered.length === 0;\n }\n\n get entries(): readonly RegisteredGradientFill[] {\n return this.registered;\n }\n}\n\n/**\n * backgroundGradient 属性値をパースしてレジストリに登録し、マーカー色を返す。\n * パースできない場合 (スキーマ検証済みのため通常発生しない) は undefined を返す。\n */\nexport function registerBackgroundGradient(\n value: string,\n opacity: number | undefined,\n registry: GradientFillRegistry,\n): string | undefined {\n const gradient = parseLinearGradient(value);\n if (!gradient) return undefined;\n return registry.register(gradient, opacity);\n}\n\n/**\n * LinearGradient を DrawingML の `<a:gradFill>` 要素に変換する\n *\n * - カラーストップ位置: % → 1/1000 % (0-100000)\n * - 角度: CSS 基準 (0deg = 上向き) → DrawingML 基準 (0 = 右向き、1/60000 度)\n */\nfunction buildGradFillXml(gradient: LinearGradient, opacity?: number): string {\n const alphaXml =\n opacity !== undefined\n ? `<a:alpha val=\"${Math.round(opacity * 100000)}\"/>`\n : \"\";\n const gsXml = gradient.stops\n .map((stop) => {\n const pos = Math.round(stop.position * 1000);\n const srgbClr = alphaXml\n ? `<a:srgbClr val=\"${stop.color}\">${alphaXml}</a:srgbClr>`\n : `<a:srgbClr val=\"${stop.color}\"/>`;\n return `<a:gs pos=\"${pos}\">${srgbClr}</a:gs>`;\n })\n .join(\"\");\n const dmlAngle = (((gradient.angle - 90) % 360) + 360) % 360;\n const ang = Math.round(dmlAngle * 60000);\n return `<a:gradFill flip=\"none\" rotWithShape=\"1\"><a:gsLst>${gsXml}</a:gsLst><a:lin ang=\"${ang}\" scaled=\"0\"/></a:gradFill>`;\n}\n\n/**\n * 出力 zip 内のスライド XML のマーカー solidFill を gradFill に置換する\n */\nexport async function applyGradientFills(\n data: Uint8Array | ArrayBuffer,\n registry: GradientFillRegistry,\n): Promise<import(\"jszip\")> {\n const JSZip = await loadJSZip();\n const zip = await JSZip.loadAsync(data);\n\n const slidePaths = Object.keys(zip.files).filter((path) =>\n /^ppt\\/slides\\/slide\\d+\\.xml$/.test(path),\n );\n for (const path of slidePaths) {\n const file = zip.file(path);\n if (!file) continue;\n let xml = await file.async(\"text\");\n let replaced = false;\n for (const { marker, gradient, opacity } of registry.entries) {\n const target = `<a:solidFill><a:srgbClr val=\"${marker}\"/></a:solidFill>`;\n if (!xml.includes(target)) continue;\n xml = xml.replaceAll(target, buildGradFillXml(gradient, opacity));\n replaced = true;\n }\n if (replaced) {\n zip.file(path, xml);\n }\n }\n return zip;\n}\n\n/**\n * pptx インスタンスの write / writeFile をラップし、\n * 出力時にグラデーション後処理を適用する。\n *\n * pptxgenjs の write と同じ outputType / compression の挙動を再現する。\n * writeFile は Node 環境のみ後処理対象 (ブラウザでは pptxgenjs がダウンロード\n * 処理を行うため、元の実装にフォールバックする)。\n */\nexport function patchPptxWriteForGradientFills(\n pptx: PptxGenJSInstance,\n registry: GradientFillRegistry,\n): void {\n if (registry.isEmpty) return;\n\n const originalWrite = pptx.write.bind(pptx);\n const originalWriteFile = pptx.writeFile.bind(pptx);\n\n const patchedWrite = async (rawProps?: WriteProps | string) => {\n // DEPRECATED: pptxgenjs は write(outputType) の文字列 overload を\n // ランタイムでは今も受け付けるため、同様に正規化する\n const props: WriteProps | undefined =\n typeof rawProps === \"string\"\n ? ({ outputType: rawProps } as WriteProps)\n : rawProps;\n const data = (await originalWrite({\n outputType: \"uint8array\",\n })) as Uint8Array;\n const zip = await applyGradientFills(data, registry);\n\n const outputType = props?.outputType;\n if (outputType === \"STREAM\") {\n return zip.generateAsync({\n type: \"nodebuffer\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n }\n if (outputType) {\n return zip.generateAsync({ type: outputType });\n }\n return zip.generateAsync({\n type: \"blob\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n };\n pptx.write = patchedWrite;\n\n const patchedWriteFile = async (rawProps?: WriteFileProps | string) => {\n // DEPRECATED: pptxgenjs は writeFile(fileName) の文字列 overload を\n // ランタイムでは今も受け付けるため、同様に正規化する\n const props: WriteFileProps | undefined =\n typeof rawProps === \"string\" ? { fileName: rawProps } : rawProps;\n const isNode =\n typeof process !== \"undefined\" && Boolean(process.versions?.node);\n if (!isNode) {\n return originalWriteFile(props);\n }\n const rawName = props?.fileName ?? \"Presentation.pptx\";\n const fileName = rawName.toLowerCase().endsWith(\".pptx\")\n ? rawName\n : `${rawName}.pptx`;\n const buffer = (await patchedWrite({\n outputType: \"nodebuffer\",\n compression: props?.compression,\n })) as Buffer;\n const fs = await import(\"fs\");\n await fs.promises.writeFile(fileName, buffer);\n return fileName;\n };\n pptx.writeFile = patchedWriteFile;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gradientFills.js","names":[],"sources":["../../src/renderPptx/gradientFills.ts"],"sourcesContent":["/**\n * グラデーション塗りの実現\n *\n * pptxgenjs はグラデーション塗りを未サポート\n * (https://github.com/gitbrent/PptxGenJS/issues/102) のため、以下の方式で実現する:\n *\n * 1. レンダリング時: グラデーション指定のある shape をビルド内で一意なマーカー色の\n * 単色塗り (solidFill) として描画し、レジストリに登録する\n * 2. 出力時: pptx.write() / writeFile() をラップし、出力 zip 内のスライド XML の\n * `<a:solidFill><a:srgbClr val=\"マーカー色\"/></a:solidFill>` を\n * DrawingML ネイティブの `<a:gradFill>` に置換する\n *\n * 置換は pptxgenjs が生成する固定パターンに対する完全一致の文字列置換で行う。\n * スライド XML 全体をパーサーで往復させると無関係な要素の表現が変わり得るため、\n * 意図的に文字列置換を採用している。\n */\nimport type { Gradient } from \"../shared/gradient.ts\";\nimport { parseGradient, parseLinearGradient } from \"../shared/gradient.ts\";\n\ntype PptxGenJSInstance = import(\"pptxgenjs\").default;\ntype WriteProps = NonNullable<Parameters<PptxGenJSInstance[\"write\"]>[0]>;\ntype WriteFileProps = NonNullable<\n Parameters<PptxGenJSInstance[\"writeFile\"]>[0]\n>;\n\n// JSZip は CJS パッケージのため動的 import で読み込む\nasync function loadJSZip(): Promise<typeof import(\"jszip\")> {\n const mod = await import(\"jszip\");\n /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n return (mod as any).default ?? mod;\n /* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n}\n\ninterface RegisteredGradientFill {\n /** マーカー色 (6桁大文字 HEX、# なし) */\n marker: string;\n gradient: Gradient;\n /** 0-1。指定時は各カラーストップに alpha として反映する */\n opacity?: number;\n}\n\n/** マーカー色の探索開始値。reserveColors で予約済みの色はスキップされる */\nconst MARKER_BASE = 0x0f7a3d;\n\nexport class GradientFillRegistry {\n private readonly reserved = new Set<string>();\n private readonly markerBySpec = new Map<string, string>();\n private readonly registered: RegisteredGradientFill[] = [];\n private nextCandidate = MARKER_BASE;\n\n /**\n * テキスト中に現れる 6桁 HEX をマーカー候補から除外する。\n * 入力 XML 由来のユーザー指定色とマーカーの衝突を防ぐため、\n * register より前に入力 XML 文字列を渡しておく。\n */\n reserveColors(text: string): void {\n for (const match of text.matchAll(/[0-9a-fA-F]{6}/g)) {\n this.reserved.add(match[0].toUpperCase());\n }\n }\n\n /** グラデーションを登録し、対応するマーカー色を返す */\n register(gradient: Gradient, opacity?: number): string {\n const specKey = JSON.stringify({ gradient, opacity });\n const existing = this.markerBySpec.get(specKey);\n if (existing) return existing;\n\n let marker: string;\n do {\n marker = this.nextCandidate.toString(16).toUpperCase().padStart(6, \"0\");\n this.nextCandidate = (this.nextCandidate + 1) % 0x1000000;\n } while (this.reserved.has(marker));\n this.reserved.add(marker);\n\n this.markerBySpec.set(specKey, marker);\n this.registered.push({ marker, gradient, opacity });\n return marker;\n }\n\n get isEmpty(): boolean {\n return this.registered.length === 0;\n }\n\n get entries(): readonly RegisteredGradientFill[] {\n return this.registered;\n }\n}\n\n/**\n * backgroundGradient 属性値をパースしてレジストリに登録し、マーカー色を返す。\n * linear-gradient / radial-gradient の両方に対応。\n * パースできない場合 (スキーマ検証済みのため通常発生しない) は undefined を返す。\n */\nexport function registerBackgroundGradient(\n value: string,\n opacity: number | undefined,\n registry: GradientFillRegistry,\n): string | undefined {\n const gradient = parseGradient(value);\n if (!gradient) return undefined;\n return registry.register(gradient, opacity);\n}\n\n/**\n * textGradient 属性値をパースしてレジストリに登録し、マーカー色を返す。\n * 戻り値のマーカー色を text run の color に渡すと、pptxgenjs が出力する\n * `<a:rPr><a:solidFill><a:srgbClr val=\"マーカー色\"/></a:solidFill></a:rPr>` が\n * 後処理で gradFill に置換され、PowerPoint 上ネイティブの文字グラデーションとして\n * 表示・編集可能になる。\n *\n * textGradient は radial-gradient を受け付けない (linear-gradient のみ)。\n * パースできない場合 (スキーマ検証済みのため通常発生しない) は undefined を返す。\n */\nexport function registerTextGradient(\n value: string,\n registry: GradientFillRegistry,\n): string | undefined {\n const linear = parseLinearGradient(value);\n if (!linear) return undefined;\n return registry.register({ kind: \"linear\", value: linear });\n}\n\n/**\n * Gradient を DrawingML の `<a:gradFill>` 要素に変換する\n *\n * - カラーストップ位置: % → 1/1000 % (0-100000)\n * - 角度 (linear): CSS 基準 (0deg = 上向き) → DrawingML 基準 (0 = 右向き、1/60000 度)\n * - 中心位置 (radial): CSS 風 % → DrawingML `<a:fillToRect l/t/r/b>` (1/1000 %)。\n * fillToRect は焦点を表す矩形で、中心位置 (cx, cy) に対し\n * l=cx*1000 / t=cy*1000 / r=(100-cx)*1000 / b=(100-cy)*1000 とする。\n * PowerPoint の radial fill は path=\"circle\" 1 種類で、shape (circle / ellipse) や\n * size キーワードを描画上区別しない。要素の縦横比に応じて自動で楕円状になる。\n */\nfunction buildGradFillXml(gradient: Gradient, opacity?: number): string {\n const alphaXml =\n opacity !== undefined\n ? `<a:alpha val=\"${Math.round(opacity * 100000)}\"/>`\n : \"\";\n const gsXml = gradient.value.stops\n .map((stop) => {\n const pos = Math.round(stop.position * 1000);\n const srgbClr = alphaXml\n ? `<a:srgbClr val=\"${stop.color}\">${alphaXml}</a:srgbClr>`\n : `<a:srgbClr val=\"${stop.color}\"/>`;\n return `<a:gs pos=\"${pos}\">${srgbClr}</a:gs>`;\n })\n .join(\"\");\n\n if (gradient.kind === \"linear\") {\n const dmlAngle = (((gradient.value.angle - 90) % 360) + 360) % 360;\n const ang = Math.round(dmlAngle * 60000);\n return `<a:gradFill flip=\"none\" rotWithShape=\"1\"><a:gsLst>${gsXml}</a:gsLst><a:lin ang=\"${ang}\" scaled=\"0\"/></a:gradFill>`;\n }\n\n const { centerX, centerY } = gradient.value;\n const l = Math.round(centerX * 1000);\n const t = Math.round(centerY * 1000);\n const r = Math.round((100 - centerX) * 1000);\n const b = Math.round((100 - centerY) * 1000);\n return `<a:gradFill flip=\"none\" rotWithShape=\"1\"><a:gsLst>${gsXml}</a:gsLst><a:path path=\"circle\"><a:fillToRect l=\"${l}\" t=\"${t}\" r=\"${r}\" b=\"${b}\"/></a:path></a:gradFill>`;\n}\n\n/**\n * 出力 zip 内のスライド XML のマーカー solidFill を gradFill に置換する\n */\nexport async function applyGradientFills(\n data: Uint8Array | ArrayBuffer,\n registry: GradientFillRegistry,\n): Promise<import(\"jszip\")> {\n const JSZip = await loadJSZip();\n const zip = await JSZip.loadAsync(data);\n\n const slidePaths = Object.keys(zip.files).filter((path) =>\n /^ppt\\/slides\\/slide\\d+\\.xml$/.test(path),\n );\n for (const path of slidePaths) {\n const file = zip.file(path);\n if (!file) continue;\n let xml = await file.async(\"text\");\n let replaced = false;\n for (const { marker, gradient, opacity } of registry.entries) {\n const target = `<a:solidFill><a:srgbClr val=\"${marker}\"/></a:solidFill>`;\n if (!xml.includes(target)) continue;\n xml = xml.replaceAll(target, buildGradFillXml(gradient, opacity));\n replaced = true;\n }\n if (replaced) {\n zip.file(path, xml);\n }\n }\n return zip;\n}\n\n/**\n * pptx インスタンスの write / writeFile をラップし、\n * 出力時にグラデーション後処理を適用する。\n *\n * pptxgenjs の write と同じ outputType / compression の挙動を再現する。\n * writeFile は Node 環境のみ後処理対象 (ブラウザでは pptxgenjs がダウンロード\n * 処理を行うため、元の実装にフォールバックする)。\n */\nexport function patchPptxWriteForGradientFills(\n pptx: PptxGenJSInstance,\n registry: GradientFillRegistry,\n): void {\n if (registry.isEmpty) return;\n\n const originalWrite = pptx.write.bind(pptx);\n const originalWriteFile = pptx.writeFile.bind(pptx);\n\n const patchedWrite = async (rawProps?: WriteProps | string) => {\n // DEPRECATED: pptxgenjs は write(outputType) の文字列 overload を\n // ランタイムでは今も受け付けるため、同様に正規化する\n const props: WriteProps | undefined =\n typeof rawProps === \"string\"\n ? ({ outputType: rawProps } as WriteProps)\n : rawProps;\n const data = (await originalWrite({\n outputType: \"uint8array\",\n })) as Uint8Array;\n const zip = await applyGradientFills(data, registry);\n\n const outputType = props?.outputType;\n if (outputType === \"STREAM\") {\n return zip.generateAsync({\n type: \"nodebuffer\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n }\n if (outputType) {\n return zip.generateAsync({ type: outputType });\n }\n return zip.generateAsync({\n type: \"blob\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n };\n pptx.write = patchedWrite;\n\n const patchedWriteFile = async (rawProps?: WriteFileProps | string) => {\n // DEPRECATED: pptxgenjs は writeFile(fileName) の文字列 overload を\n // ランタイムでは今も受け付けるため、同様に正規化する\n const props: WriteFileProps | undefined =\n typeof rawProps === \"string\" ? { fileName: rawProps } : rawProps;\n const isNode =\n typeof process !== \"undefined\" && Boolean(process.versions?.node);\n if (!isNode) {\n return originalWriteFile(props);\n }\n const rawName = props?.fileName ?? \"Presentation.pptx\";\n const fileName = rawName.toLowerCase().endsWith(\".pptx\")\n ? rawName\n : `${rawName}.pptx`;\n const buffer = (await patchedWrite({\n outputType: \"nodebuffer\",\n compression: props?.compression,\n })) as Buffer;\n const fs = await import(\"fs\");\n await fs.promises.writeFile(fileName, buffer);\n return fileName;\n };\n pptx.writeFile = patchedWriteFile;\n}\n"],"mappings":"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"}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { pxToIn } from "../units.js";
|
|
1
|
+
import { pxToIn, pxToPt } from "../units.js";
|
|
2
2
|
//#region src/renderPptx/nodes/icon.ts
|
|
3
3
|
function renderIconNode(node, ctx) {
|
|
4
4
|
if (node.variant) {
|
|
5
5
|
const isCircle = node.variant.startsWith("circle");
|
|
6
6
|
const isFilled = node.variant.endsWith("-filled");
|
|
7
7
|
const colorValue = (node.bgColor ?? "#E0E0E0").replace(/^#/, "");
|
|
8
|
+
const variantDefaultLine = isFilled ? void 0 : {
|
|
9
|
+
color: colorValue,
|
|
10
|
+
width: 1.5
|
|
11
|
+
};
|
|
12
|
+
const outlineLine = node.outline ? {
|
|
13
|
+
color: node.outline.color ?? variantDefaultLine?.color ?? "FFFFFF",
|
|
14
|
+
width: node.outline.size !== void 0 ? pxToPt(node.outline.size) : variantDefaultLine?.width ?? 1
|
|
15
|
+
} : variantDefaultLine;
|
|
16
|
+
const glowMarker = node.glow ? ctx.buildContext.glowEffects.register(node.glow) : void 0;
|
|
8
17
|
const shapeType = isCircle ? "ellipse" : "roundRect";
|
|
9
18
|
const shapeOptions = {
|
|
10
19
|
x: pxToIn(node.bgX ?? node.x),
|
|
@@ -12,12 +21,10 @@ function renderIconNode(node, ctx) {
|
|
|
12
21
|
w: pxToIn(node.bgW ?? node.w),
|
|
13
22
|
h: pxToIn(node.bgH ?? node.h),
|
|
14
23
|
fill: isFilled ? { color: colorValue } : { type: "none" },
|
|
15
|
-
line:
|
|
16
|
-
color: colorValue,
|
|
17
|
-
width: 1.5
|
|
18
|
-
},
|
|
24
|
+
line: outlineLine,
|
|
19
25
|
rectRadius: isCircle ? void 0 : .1,
|
|
20
|
-
rotate: node.rotate
|
|
26
|
+
rotate: node.rotate,
|
|
27
|
+
objectName: glowMarker
|
|
21
28
|
};
|
|
22
29
|
ctx.slide.addShape(shapeType, shapeOptions);
|
|
23
30
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"icon.js","names":[],"sources":["../../../src/renderPptx/nodes/icon.ts"],"sourcesContent":["import type { PositionedNode } from \"../../types.ts\";\nimport type { RenderContext } from \"../types.ts\";\nimport { pxToIn } from \"../units.ts\";\n\ntype IconPositionedNode = Extract<PositionedNode, { type: \"icon\" }>;\n\nexport function renderIconNode(\n node: IconPositionedNode,\n ctx: RenderContext,\n): void {\n // variant 指定時は背景図形を描画\n if (node.variant) {\n const isCircle = node.variant.startsWith(\"circle\");\n const isFilled = node.variant.endsWith(\"-filled\");\n const bgColor = node.bgColor ?? \"#E0E0E0\";\n const colorValue = bgColor.replace(/^#/, \"\");\n\n const shapeType = isCircle ? \"ellipse\" : \"roundRect\";\n const shapeOptions: Record<string, unknown> = {\n x: pxToIn(node.bgX ?? node.x),\n y: pxToIn(node.bgY ?? node.y),\n w: pxToIn(node.bgW ?? node.w),\n h: pxToIn(node.bgH ?? node.h),\n fill: isFilled ? { color: colorValue } : { type: \"none\" as const },\n line:
|
|
1
|
+
{"version":3,"file":"icon.js","names":[],"sources":["../../../src/renderPptx/nodes/icon.ts"],"sourcesContent":["import type { PositionedNode } from \"../../types.ts\";\nimport type { RenderContext } from \"../types.ts\";\nimport { pxToIn, pxToPt } from \"../units.ts\";\n\ntype IconPositionedNode = Extract<PositionedNode, { type: \"icon\" }>;\n\nexport function renderIconNode(\n node: IconPositionedNode,\n ctx: RenderContext,\n): void {\n // variant 指定時は背景図形を描画\n if (node.variant) {\n const isCircle = node.variant.startsWith(\"circle\");\n const isFilled = node.variant.endsWith(\"-filled\");\n const bgColor = node.bgColor ?? \"#E0E0E0\";\n const colorValue = bgColor.replace(/^#/, \"\");\n\n // 背景図形の line のデフォルト: outlined variant は colorValue / 1.5pt、\n // filled variant は undefined (枠線なし)。\n const variantDefaultLine = isFilled\n ? undefined\n : { color: colorValue, width: 1.5 };\n // outline 指定時は variant のデフォルト line とフィールド単位でマージする。\n // outline 側で省略された属性は variant default の値を引き継ぐので、例えば\n // outlined variant に `outline.color` だけ指定すると、太さは 1.5pt のまま\n // 色だけ outline で上書きされる。\n const outlineLine = node.outline\n ? {\n color: node.outline.color ?? variantDefaultLine?.color ?? \"FFFFFF\",\n width:\n node.outline.size !== undefined\n ? pxToPt(node.outline.size)\n : (variantDefaultLine?.width ?? 1),\n }\n : variantDefaultLine;\n\n const glowMarker = node.glow\n ? ctx.buildContext.glowEffects.register(node.glow)\n : undefined;\n\n const shapeType = isCircle ? \"ellipse\" : \"roundRect\";\n const shapeOptions: Record<string, unknown> = {\n x: pxToIn(node.bgX ?? node.x),\n y: pxToIn(node.bgY ?? node.y),\n w: pxToIn(node.bgW ?? node.w),\n h: pxToIn(node.bgH ?? node.h),\n fill: isFilled ? { color: colorValue } : { type: \"none\" as const },\n line: outlineLine,\n rectRadius: isCircle ? undefined : 0.1,\n rotate: node.rotate,\n objectName: glowMarker,\n };\n\n ctx.slide.addShape(shapeType, shapeOptions);\n }\n\n ctx.slide.addImage({\n data: node.iconImageData,\n x: pxToIn(node.iconX ?? node.x),\n y: pxToIn(node.iconY ?? node.y),\n w: pxToIn(node.iconW ?? node.w),\n h: pxToIn(node.iconH ?? node.h),\n rotate: node.rotate,\n });\n}\n"],"mappings":";;AAMA,SAAgB,eACd,MACA,KACM;CAEN,IAAI,KAAK,SAAS;EAChB,MAAM,WAAW,KAAK,QAAQ,WAAW,QAAQ;EACjD,MAAM,WAAW,KAAK,QAAQ,SAAS,SAAS;EAEhD,MAAM,cADU,KAAK,WAAW,UAAA,CACL,QAAQ,MAAM,EAAE;EAI3C,MAAM,qBAAqB,WACvB,KAAA,IACA;GAAE,OAAO;GAAY,OAAO;EAAI;EAKpC,MAAM,cAAc,KAAK,UACrB;GACE,OAAO,KAAK,QAAQ,SAAS,oBAAoB,SAAS;GAC1D,OACE,KAAK,QAAQ,SAAS,KAAA,IAClB,OAAO,KAAK,QAAQ,IAAI,IACvB,oBAAoB,SAAS;EACtC,IACA;EAEJ,MAAM,aAAa,KAAK,OACpB,IAAI,aAAa,YAAY,SAAS,KAAK,IAAI,IAC/C,KAAA;EAEJ,MAAM,YAAY,WAAW,YAAY;EACzC,MAAM,eAAwC;GAC5C,GAAG,OAAO,KAAK,OAAO,KAAK,CAAC;GAC5B,GAAG,OAAO,KAAK,OAAO,KAAK,CAAC;GAC5B,GAAG,OAAO,KAAK,OAAO,KAAK,CAAC;GAC5B,GAAG,OAAO,KAAK,OAAO,KAAK,CAAC;GAC5B,MAAM,WAAW,EAAE,OAAO,WAAW,IAAI,EAAE,MAAM,OAAgB;GACjE,MAAM;GACN,YAAY,WAAW,KAAA,IAAY;GACnC,QAAQ,KAAK;GACb,YAAY;EACd;EAEA,IAAI,MAAM,SAAS,WAAW,YAAY;CAC5C;CAEA,IAAI,MAAM,SAAS;EACjB,MAAM,KAAK;EACX,GAAG,OAAO,KAAK,SAAS,KAAK,CAAC;EAC9B,GAAG,OAAO,KAAK,SAAS,KAAK,CAAC;EAC9B,GAAG,OAAO,KAAK,SAAS,KAAK,CAAC;EAC9B,GAAG,OAAO,KAAK,SAAS,KAAK,CAAC;EAC9B,QAAQ,KAAK;CACf,CAAC;AACH"}
|
|
@@ -3,16 +3,38 @@ import { getContentAreaIn } from "../utils/contentArea.js";
|
|
|
3
3
|
import { convertStrike, convertUnderline } from "../textOptions.js";
|
|
4
4
|
import { convertBorderLine, convertShadow } from "../utils/visualStyle.js";
|
|
5
5
|
//#region src/renderPptx/nodes/shape.ts
|
|
6
|
+
/**
|
|
7
|
+
* outline (Text と同じ書式の `outline.size` / `outline.color`) と
|
|
8
|
+
* 既存 `line` 属性 (`line.color` / `line.width` / `line.dashType`) を
|
|
9
|
+
* 1 つの BorderStyle にマージする。
|
|
10
|
+
*
|
|
11
|
+
* フィールド単位のマージで、`outline` の指定があるフィールドは `line` を
|
|
12
|
+
* 上書きするが、`outline` 側で省略されたフィールドは `line` の値を引き継ぎ、
|
|
13
|
+
* `line` にも値が無い場合は Text outline と同じ既定値 (`width: 1pt 相当` /
|
|
14
|
+
* `color: FFFFFF`) を採用する。`dashType` は `outline` に対応フィールドが
|
|
15
|
+
* 無いため `line.dashType` をそのまま使う。
|
|
16
|
+
*/
|
|
17
|
+
function resolveShapeLine(line, outline) {
|
|
18
|
+
if (!outline) return line;
|
|
19
|
+
return {
|
|
20
|
+
color: outline.color ?? line?.color ?? "FFFFFF",
|
|
21
|
+
width: outline.size ?? line?.width ?? 1,
|
|
22
|
+
dashType: line?.dashType
|
|
23
|
+
};
|
|
24
|
+
}
|
|
6
25
|
function renderShapeNode(node, ctx) {
|
|
26
|
+
const lineSpec = resolveShapeLine(node.line, node.outline);
|
|
27
|
+
const glowMarker = node.glow ? ctx.buildContext.glowEffects.register(node.glow) : void 0;
|
|
7
28
|
const shapeOptions = {
|
|
8
29
|
...getContentAreaIn(node),
|
|
9
30
|
fill: node.fill ? {
|
|
10
31
|
color: node.fill.color,
|
|
11
32
|
transparency: node.fill.transparency
|
|
12
33
|
} : void 0,
|
|
13
|
-
line:
|
|
34
|
+
line: lineSpec ? convertBorderLine(lineSpec) : void 0,
|
|
14
35
|
shadow: convertShadow(node.shadow),
|
|
15
|
-
rotate: node.rotate
|
|
36
|
+
rotate: node.rotate,
|
|
37
|
+
objectName: glowMarker
|
|
16
38
|
};
|
|
17
39
|
if (node.text) {
|
|
18
40
|
const fontSizePx = node.fontSize ?? 24;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shape.js","names":[],"sources":["../../../src/renderPptx/nodes/shape.ts"],"sourcesContent":["import type { PositionedNode } from \"../../types.ts\";\nimport type { RenderContext } from \"../types.ts\";\nimport { pxToPt } from \"../units.ts\";\nimport { convertUnderline, convertStrike } from \"../textOptions.ts\";\nimport { getContentAreaIn } from \"../utils/contentArea.ts\";\nimport { convertBorderLine, convertShadow } from \"../utils/visualStyle.ts\";\n\ntype ShapePositionedNode = Extract<PositionedNode, { type: \"shape\" }>;\n\nexport function renderShapeNode(\n node: ShapePositionedNode,\n ctx: RenderContext,\n): void {\n const shapeOptions = {\n ...getContentAreaIn(node),\n fill: node.fill\n ? {\n color: node.fill.color,\n transparency: node.fill.transparency,\n }\n : undefined,\n line:
|
|
1
|
+
{"version":3,"file":"shape.js","names":[],"sources":["../../../src/renderPptx/nodes/shape.ts"],"sourcesContent":["import type { BorderStyle, PositionedNode } from \"../../types.ts\";\nimport type { RenderContext } from \"../types.ts\";\nimport { pxToPt } from \"../units.ts\";\nimport { convertUnderline, convertStrike } from \"../textOptions.ts\";\nimport { getContentAreaIn } from \"../utils/contentArea.ts\";\nimport { convertBorderLine, convertShadow } from \"../utils/visualStyle.ts\";\n\ntype ShapePositionedNode = Extract<PositionedNode, { type: \"shape\" }>;\n\n/**\n * outline (Text と同じ書式の `outline.size` / `outline.color`) と\n * 既存 `line` 属性 (`line.color` / `line.width` / `line.dashType`) を\n * 1 つの BorderStyle にマージする。\n *\n * フィールド単位のマージで、`outline` の指定があるフィールドは `line` を\n * 上書きするが、`outline` 側で省略されたフィールドは `line` の値を引き継ぎ、\n * `line` にも値が無い場合は Text outline と同じ既定値 (`width: 1pt 相当` /\n * `color: FFFFFF`) を採用する。`dashType` は `outline` に対応フィールドが\n * 無いため `line.dashType` をそのまま使う。\n */\nfunction resolveShapeLine(\n line: BorderStyle | undefined,\n outline: { size?: number; color?: string } | undefined,\n): BorderStyle | undefined {\n if (!outline) return line;\n return {\n color: outline.color ?? line?.color ?? \"FFFFFF\",\n width: outline.size ?? line?.width ?? 1,\n dashType: line?.dashType,\n };\n}\n\nexport function renderShapeNode(\n node: ShapePositionedNode,\n ctx: RenderContext,\n): void {\n const lineSpec = resolveShapeLine(node.line, node.outline);\n const glowMarker = node.glow\n ? ctx.buildContext.glowEffects.register(node.glow)\n : undefined;\n\n const shapeOptions = {\n ...getContentAreaIn(node),\n fill: node.fill\n ? {\n color: node.fill.color,\n transparency: node.fill.transparency,\n }\n : undefined,\n line: lineSpec ? convertBorderLine(lineSpec) : undefined,\n shadow: convertShadow(node.shadow),\n rotate: node.rotate,\n objectName: glowMarker,\n };\n\n if (node.text) {\n const fontSizePx = node.fontSize ?? 24;\n const lineHeight = node.lineHeight ?? 1.3;\n // テキストがある場合:addTextでshapeを指定\n ctx.slide.addText(node.text, {\n ...shapeOptions,\n shape: node.shapeType,\n fontSize: pxToPt(fontSizePx),\n fontFace: node.fontFamily ?? \"Noto Sans JP\",\n color: node.color,\n bold: node.bold,\n italic: node.italic,\n underline: convertUnderline(node.underline),\n strike: convertStrike(node.strike),\n subscript: node.subscript,\n superscript: node.superscript,\n highlight: node.highlight,\n align: node.textAlign ?? \"center\",\n valign: \"middle\" as const,\n // Text と同じく行送りを固定値 (spcPts) で指定し、計測高さ\n // (行数 × fontSize × lineHeight) と実描画の行高さを一致させる (#846)。\n // valign middle のためテキストブロックは枠内中央に配置され、\n // Text のような描画 y 補正は不要\n lineSpacing: pxToPt(fontSizePx * lineHeight),\n });\n } else {\n // テキストがない場合:addShapeを使用\n ctx.slide.addShape(node.shapeType, shapeOptions);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAoBA,SAAS,iBACP,MACA,SACyB;CACzB,IAAI,CAAC,SAAS,OAAO;CACrB,OAAO;EACL,OAAO,QAAQ,SAAS,MAAM,SAAS;EACvC,OAAO,QAAQ,QAAQ,MAAM,SAAS;EACtC,UAAU,MAAM;CAClB;AACF;AAEA,SAAgB,gBACd,MACA,KACM;CACN,MAAM,WAAW,iBAAiB,KAAK,MAAM,KAAK,OAAO;CACzD,MAAM,aAAa,KAAK,OACpB,IAAI,aAAa,YAAY,SAAS,KAAK,IAAI,IAC/C,KAAA;CAEJ,MAAM,eAAe;EACnB,GAAG,iBAAiB,IAAI;EACxB,MAAM,KAAK,OACP;GACE,OAAO,KAAK,KAAK;GACjB,cAAc,KAAK,KAAK;EAC1B,IACA,KAAA;EACJ,MAAM,WAAW,kBAAkB,QAAQ,IAAI,KAAA;EAC/C,QAAQ,cAAc,KAAK,MAAM;EACjC,QAAQ,KAAK;EACb,YAAY;CACd;CAEA,IAAI,KAAK,MAAM;EACb,MAAM,aAAa,KAAK,YAAY;EACpC,MAAM,aAAa,KAAK,cAAc;EAEtC,IAAI,MAAM,QAAQ,KAAK,MAAM;GAC3B,GAAG;GACH,OAAO,KAAK;GACZ,UAAU,OAAO,UAAU;GAC3B,UAAU,KAAK,cAAc;GAC7B,OAAO,KAAK;GACZ,MAAM,KAAK;GACX,QAAQ,KAAK;GACb,WAAW,iBAAiB,KAAK,SAAS;GAC1C,QAAQ,cAAc,KAAK,MAAM;GACjC,WAAW,KAAK;GAChB,aAAa,KAAK;GAClB,WAAW,KAAK;GAChB,OAAO,KAAK,aAAa;GACzB,QAAQ;GAKR,aAAa,OAAO,aAAa,UAAU;EAC7C,CAAC;CACH,OAEE,IAAI,MAAM,SAAS,KAAK,WAAW,YAAY;AAEnD"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { pxToPt } from "../units.js";
|
|
2
2
|
import { convertGlow, convertOutline, convertStrike, convertUnderline, createTextOptions, resolveSubSup } from "../textOptions.js";
|
|
3
|
+
import { registerTextGradient } from "../gradientFills.js";
|
|
3
4
|
//#region src/renderPptx/nodes/text.ts
|
|
4
5
|
function renderTextNode(node, ctx) {
|
|
5
6
|
const textOptions = createTextOptions(node);
|
|
7
|
+
const textGradientMarker = node.textGradient ? registerTextGradient(node.textGradient, ctx.buildContext.gradientFills) : void 0;
|
|
6
8
|
if (node.runs && node.runs.length > 0) {
|
|
7
9
|
const fontSizePx = node.fontSize ?? 24;
|
|
8
10
|
const fontFamily = node.fontFamily ?? "Noto Sans JP";
|
|
@@ -15,7 +17,7 @@ function renderTextNode(node, ctx) {
|
|
|
15
17
|
options: {
|
|
16
18
|
fontSize: pxToPt(runFontSizePx),
|
|
17
19
|
fontFace: run.fontFamily ?? fontFamily,
|
|
18
|
-
color: run.color ?? node.color,
|
|
20
|
+
color: textGradientMarker ?? run.color ?? node.color,
|
|
19
21
|
bold: run.bold ?? node.bold,
|
|
20
22
|
italic: run.italic ?? node.italic,
|
|
21
23
|
underline: convertUnderline(run.underline ?? node.underline),
|
|
@@ -41,7 +43,10 @@ function renderTextNode(node, ctx) {
|
|
|
41
43
|
margin: textOptions.margin,
|
|
42
44
|
lineSpacing: textOptions.lineSpacing
|
|
43
45
|
});
|
|
44
|
-
} else ctx.slide.addText(node.text ?? "",
|
|
46
|
+
} else ctx.slide.addText(node.text ?? "", {
|
|
47
|
+
...textOptions,
|
|
48
|
+
color: textGradientMarker ?? textOptions.color
|
|
49
|
+
});
|
|
45
50
|
}
|
|
46
51
|
//#endregion
|
|
47
52
|
export { renderTextNode };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.js","names":[],"sources":["../../../src/renderPptx/nodes/text.ts"],"sourcesContent":["import type { PositionedNode } from \"../../types.ts\";\nimport type { RenderContext } from \"../types.ts\";\nimport {\n createTextOptions,\n convertUnderline,\n convertStrike,\n convertGlow,\n convertOutline,\n resolveSubSup,\n} from \"../textOptions.ts\";\nimport { pxToPt } from \"../units.ts\";\n\ntype TextPositionedNode = Extract<PositionedNode, { type: \"text\" }>;\n\nexport function renderTextNode(\n node: TextPositionedNode,\n ctx: RenderContext,\n): void {\n const textOptions = createTextOptions(node);\n\n if (node.runs && node.runs.length > 0) {\n const fontSizePx = node.fontSize ?? 24;\n const fontFamily = node.fontFamily ?? \"Noto Sans JP\";\n const textItems = node.runs.map((run) => {\n const letterSpacingPx = run.letterSpacing ?? node.letterSpacing;\n const runFontSizePx = run.fontSize ?? fontSizePx;\n const subSup = resolveSubSup(run, node);\n return {\n text: run.text,\n options: {\n fontSize: pxToPt(runFontSizePx),\n fontFace: run.fontFamily ?? fontFamily,\n color: run.color ?? node.color,\n bold: run.bold ?? node.bold,\n italic: run.italic ?? node.italic,\n underline: convertUnderline(run.underline ?? node.underline),\n strike: convertStrike(run.strike ?? node.strike),\n subscript: subSup.subscript,\n superscript: subSup.superscript,\n highlight: run.highlight ?? node.highlight,\n // glow / outline はノード単位指定のみ (run 単位はスコープ外)\n glow: convertGlow(node.glow),\n outline: convertOutline(node.outline),\n charSpacing:\n letterSpacingPx !== undefined ? pxToPt(letterSpacingPx) : undefined,\n ...(run.href ? { hyperlink: { url: run.href } } : {}),\n },\n };\n });\n ctx.slide.addText(textItems, {\n x: textOptions.x,\n y: textOptions.y,\n w: textOptions.w,\n h: textOptions.h,\n rotate: textOptions.rotate,\n align: textOptions.align,\n valign: textOptions.valign,\n margin: textOptions.margin,\n lineSpacing: textOptions.lineSpacing,\n });\n } else {\n ctx.slide.addText(node.text ?? \"\", textOptions);\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"text.js","names":[],"sources":["../../../src/renderPptx/nodes/text.ts"],"sourcesContent":["import type { PositionedNode } from \"../../types.ts\";\nimport type { RenderContext } from \"../types.ts\";\nimport {\n createTextOptions,\n convertUnderline,\n convertStrike,\n convertGlow,\n convertOutline,\n resolveSubSup,\n} from \"../textOptions.ts\";\nimport { registerTextGradient } from \"../gradientFills.ts\";\nimport { pxToPt } from \"../units.ts\";\n\ntype TextPositionedNode = Extract<PositionedNode, { type: \"text\" }>;\n\nexport function renderTextNode(\n node: TextPositionedNode,\n ctx: RenderContext,\n): void {\n const textOptions = createTextOptions(node);\n\n // textGradient はマーカー色で text run color に適用し、\n // 出力時の後処理で gradFill に置換される (gradientFills.ts 参照)。\n // node 単位の指定として全 run の color を上書きする (run 単位指定はスコープ外)。\n const textGradientMarker = node.textGradient\n ? registerTextGradient(node.textGradient, ctx.buildContext.gradientFills)\n : undefined;\n\n if (node.runs && node.runs.length > 0) {\n const fontSizePx = node.fontSize ?? 24;\n const fontFamily = node.fontFamily ?? \"Noto Sans JP\";\n const textItems = node.runs.map((run) => {\n const letterSpacingPx = run.letterSpacing ?? node.letterSpacing;\n const runFontSizePx = run.fontSize ?? fontSizePx;\n const subSup = resolveSubSup(run, node);\n return {\n text: run.text,\n options: {\n fontSize: pxToPt(runFontSizePx),\n fontFace: run.fontFamily ?? fontFamily,\n color: textGradientMarker ?? run.color ?? node.color,\n bold: run.bold ?? node.bold,\n italic: run.italic ?? node.italic,\n underline: convertUnderline(run.underline ?? node.underline),\n strike: convertStrike(run.strike ?? node.strike),\n subscript: subSup.subscript,\n superscript: subSup.superscript,\n highlight: run.highlight ?? node.highlight,\n // glow / outline はノード単位指定のみ (run 単位はスコープ外)\n glow: convertGlow(node.glow),\n outline: convertOutline(node.outline),\n charSpacing:\n letterSpacingPx !== undefined ? pxToPt(letterSpacingPx) : undefined,\n ...(run.href ? { hyperlink: { url: run.href } } : {}),\n },\n };\n });\n ctx.slide.addText(textItems, {\n x: textOptions.x,\n y: textOptions.y,\n w: textOptions.w,\n h: textOptions.h,\n rotate: textOptions.rotate,\n align: textOptions.align,\n valign: textOptions.valign,\n margin: textOptions.margin,\n lineSpacing: textOptions.lineSpacing,\n });\n } else {\n ctx.slide.addText(node.text ?? \"\", {\n ...textOptions,\n color: textGradientMarker ?? textOptions.color,\n });\n }\n}\n"],"mappings":";;;;AAeA,SAAgB,eACd,MACA,KACM;CACN,MAAM,cAAc,kBAAkB,IAAI;CAK1C,MAAM,qBAAqB,KAAK,eAC5B,qBAAqB,KAAK,cAAc,IAAI,aAAa,aAAa,IACtE,KAAA;CAEJ,IAAI,KAAK,QAAQ,KAAK,KAAK,SAAS,GAAG;EACrC,MAAM,aAAa,KAAK,YAAY;EACpC,MAAM,aAAa,KAAK,cAAc;EACtC,MAAM,YAAY,KAAK,KAAK,KAAK,QAAQ;GACvC,MAAM,kBAAkB,IAAI,iBAAiB,KAAK;GAClD,MAAM,gBAAgB,IAAI,YAAY;GACtC,MAAM,SAAS,cAAc,KAAK,IAAI;GACtC,OAAO;IACL,MAAM,IAAI;IACV,SAAS;KACP,UAAU,OAAO,aAAa;KAC9B,UAAU,IAAI,cAAc;KAC5B,OAAO,sBAAsB,IAAI,SAAS,KAAK;KAC/C,MAAM,IAAI,QAAQ,KAAK;KACvB,QAAQ,IAAI,UAAU,KAAK;KAC3B,WAAW,iBAAiB,IAAI,aAAa,KAAK,SAAS;KAC3D,QAAQ,cAAc,IAAI,UAAU,KAAK,MAAM;KAC/C,WAAW,OAAO;KAClB,aAAa,OAAO;KACpB,WAAW,IAAI,aAAa,KAAK;KAEjC,MAAM,YAAY,KAAK,IAAI;KAC3B,SAAS,eAAe,KAAK,OAAO;KACpC,aACE,oBAAoB,KAAA,IAAY,OAAO,eAAe,IAAI,KAAA;KAC5D,GAAI,IAAI,OAAO,EAAE,WAAW,EAAE,KAAK,IAAI,KAAK,EAAE,IAAI,CAAC;IACrD;GACF;EACF,CAAC;EACD,IAAI,MAAM,QAAQ,WAAW;GAC3B,GAAG,YAAY;GACf,GAAG,YAAY;GACf,GAAG,YAAY;GACf,GAAG,YAAY;GACf,QAAQ,YAAY;GACpB,OAAO,YAAY;GACnB,QAAQ,YAAY;GACpB,QAAQ,YAAY;GACpB,aAAa,YAAY;EAC3B,CAAC;CACH,OACE,IAAI,MAAM,QAAQ,KAAK,QAAQ,IAAI;EACjC,GAAG;EACH,OAAO,sBAAsB,YAAY;CAC3C,CAAC;AAEL"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { pxToIn, pxToPt } from "../units.js";
|
|
2
2
|
import { withContentBounds } from "../utils/contentArea.js";
|
|
3
|
+
import { registerBackgroundGradient } from "../gradientFills.js";
|
|
3
4
|
import { stripHash } from "../utils/visualStyle.js";
|
|
4
5
|
import { measureTimeline } from "../../calcYogaLayout/measureCompositeNodes.js";
|
|
5
6
|
import { resolveScaledContentArea } from "../utils/scaleToFit.js";
|
|
@@ -16,14 +17,37 @@ function renderTimelineNode(node, ctx) {
|
|
|
16
17
|
title: stripHash(node.titleColor) ?? "1E293B",
|
|
17
18
|
description: stripHash(node.descriptionColor) ?? "64748B"
|
|
18
19
|
};
|
|
20
|
+
const connectorLineColor = (node.connectorGradient ? registerBackgroundGradient(node.connectorGradient, node.opacity, ctx.buildContext.gradientFills) : void 0) ?? stripHash(node.connectorColor) ?? "E2E8F0";
|
|
21
|
+
const fontFace = node.fontFamily ?? "Noto Sans JP";
|
|
22
|
+
const useColorForDate = node.useColorForDate ?? false;
|
|
19
23
|
const { content, scaleFactor } = resolveScaledContentArea(node, measureTimeline(node), ctx);
|
|
20
24
|
const nodeRadius = baseNodeRadius * scaleFactor;
|
|
21
25
|
const lineWidth = baseLineWidth * scaleFactor;
|
|
22
26
|
const contentNode = withContentBounds(node, content);
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
const options = {
|
|
28
|
+
defaultColor,
|
|
29
|
+
nodeRadius,
|
|
30
|
+
lineWidth,
|
|
31
|
+
scaleFactor,
|
|
32
|
+
textColors,
|
|
33
|
+
connectorLineColor,
|
|
34
|
+
fontFace,
|
|
35
|
+
useColorForDate
|
|
36
|
+
};
|
|
37
|
+
if (direction === "horizontal") renderHorizontalTimeline(contentNode, ctx, items, options);
|
|
38
|
+
else renderVerticalTimeline(contentNode, ctx, items, options);
|
|
39
|
+
}
|
|
40
|
+
function resolveItemDateColor(item, options) {
|
|
41
|
+
const perItemDateColor = stripHash(item.dateColor);
|
|
42
|
+
if (perItemDateColor) return perItemDateColor;
|
|
43
|
+
if (options.useColorForDate && item.color) {
|
|
44
|
+
const inherited = stripHash(item.color);
|
|
45
|
+
if (inherited) return inherited;
|
|
46
|
+
}
|
|
47
|
+
return options.textColors.date;
|
|
25
48
|
}
|
|
26
|
-
function renderHorizontalTimeline(node, ctx, items,
|
|
49
|
+
function renderHorizontalTimeline(node, ctx, items, options) {
|
|
50
|
+
const { defaultColor, nodeRadius, lineWidth, scaleFactor, textColors, connectorLineColor, fontFace } = options;
|
|
27
51
|
const itemCount = items.length;
|
|
28
52
|
const lineY = node.y + node.h / 2;
|
|
29
53
|
const labelW = 120 * scaleFactor;
|
|
@@ -36,7 +60,7 @@ function renderHorizontalTimeline(node, ctx, items, defaultColor, nodeRadius, li
|
|
|
36
60
|
w: pxToIn(lineLength),
|
|
37
61
|
h: 0,
|
|
38
62
|
line: {
|
|
39
|
-
color:
|
|
63
|
+
color: connectorLineColor,
|
|
40
64
|
width: pxToPt(lineWidth)
|
|
41
65
|
}
|
|
42
66
|
});
|
|
@@ -50,6 +74,7 @@ function renderHorizontalTimeline(node, ctx, items, defaultColor, nodeRadius, li
|
|
|
50
74
|
const cx = startX + lineLength * (itemCount === 1 ? .5 : index / (itemCount - 1));
|
|
51
75
|
const cy = lineY;
|
|
52
76
|
const color = item.color ?? defaultColor;
|
|
77
|
+
const dateColor = resolveItemDateColor(item, options);
|
|
53
78
|
ctx.slide.addShape(ctx.pptx.ShapeType.ellipse, {
|
|
54
79
|
x: pxToIn(cx - nodeRadius),
|
|
55
80
|
y: pxToIn(cy - nodeRadius),
|
|
@@ -64,8 +89,8 @@ function renderHorizontalTimeline(node, ctx, items, defaultColor, nodeRadius, li
|
|
|
64
89
|
w: pxToIn(labelW),
|
|
65
90
|
h: pxToIn(dateLabelH),
|
|
66
91
|
fontSize: pxToPt(12 * scaleFactor),
|
|
67
|
-
fontFace
|
|
68
|
-
color:
|
|
92
|
+
fontFace,
|
|
93
|
+
color: dateColor,
|
|
69
94
|
align: "center",
|
|
70
95
|
valign: "bottom"
|
|
71
96
|
});
|
|
@@ -75,7 +100,7 @@ function renderHorizontalTimeline(node, ctx, items, defaultColor, nodeRadius, li
|
|
|
75
100
|
w: pxToIn(labelW),
|
|
76
101
|
h: pxToIn(titleLabelH),
|
|
77
102
|
fontSize: pxToPt(14 * scaleFactor),
|
|
78
|
-
fontFace
|
|
103
|
+
fontFace,
|
|
79
104
|
color: textColors.title,
|
|
80
105
|
bold: true,
|
|
81
106
|
align: "center",
|
|
@@ -87,14 +112,15 @@ function renderHorizontalTimeline(node, ctx, items, defaultColor, nodeRadius, li
|
|
|
87
112
|
w: pxToIn(labelW),
|
|
88
113
|
h: pxToIn(descLabelH),
|
|
89
114
|
fontSize: pxToPt(11 * scaleFactor),
|
|
90
|
-
fontFace
|
|
115
|
+
fontFace,
|
|
91
116
|
color: textColors.description,
|
|
92
117
|
align: "center",
|
|
93
118
|
valign: "top"
|
|
94
119
|
});
|
|
95
120
|
});
|
|
96
121
|
}
|
|
97
|
-
function renderVerticalTimeline(node, ctx, items,
|
|
122
|
+
function renderVerticalTimeline(node, ctx, items, options) {
|
|
123
|
+
const { defaultColor, nodeRadius, lineWidth, scaleFactor, textColors, connectorLineColor, fontFace } = options;
|
|
98
124
|
const itemCount = items.length;
|
|
99
125
|
const lineX = node.x + 40 * scaleFactor;
|
|
100
126
|
const startY = node.y + nodeRadius;
|
|
@@ -105,7 +131,7 @@ function renderVerticalTimeline(node, ctx, items, defaultColor, nodeRadius, line
|
|
|
105
131
|
w: 0,
|
|
106
132
|
h: pxToIn(lineLength),
|
|
107
133
|
line: {
|
|
108
|
-
color:
|
|
134
|
+
color: connectorLineColor,
|
|
109
135
|
width: pxToPt(lineWidth)
|
|
110
136
|
}
|
|
111
137
|
});
|
|
@@ -121,6 +147,7 @@ function renderVerticalTimeline(node, ctx, items, defaultColor, nodeRadius, line
|
|
|
121
147
|
const cx = lineX;
|
|
122
148
|
const cy = startY + lineLength * progress;
|
|
123
149
|
const color = item.color ?? defaultColor;
|
|
150
|
+
const dateColor = resolveItemDateColor(item, options);
|
|
124
151
|
ctx.slide.addShape(ctx.pptx.ShapeType.ellipse, {
|
|
125
152
|
x: pxToIn(cx - nodeRadius),
|
|
126
153
|
y: pxToIn(cy - nodeRadius),
|
|
@@ -135,8 +162,8 @@ function renderVerticalTimeline(node, ctx, items, defaultColor, nodeRadius, line
|
|
|
135
162
|
w: pxToIn(dateLabelW),
|
|
136
163
|
h: pxToIn(dateLabelH),
|
|
137
164
|
fontSize: pxToPt(12 * scaleFactor),
|
|
138
|
-
fontFace
|
|
139
|
-
color:
|
|
165
|
+
fontFace,
|
|
166
|
+
color: dateColor,
|
|
140
167
|
align: "left",
|
|
141
168
|
valign: "bottom"
|
|
142
169
|
});
|
|
@@ -146,7 +173,7 @@ function renderVerticalTimeline(node, ctx, items, defaultColor, nodeRadius, line
|
|
|
146
173
|
w: pxToIn(titleLabelW),
|
|
147
174
|
h: pxToIn(titleLabelH),
|
|
148
175
|
fontSize: pxToPt(14 * scaleFactor),
|
|
149
|
-
fontFace
|
|
176
|
+
fontFace,
|
|
150
177
|
color: textColors.title,
|
|
151
178
|
bold: true,
|
|
152
179
|
align: "left",
|
|
@@ -158,7 +185,7 @@ function renderVerticalTimeline(node, ctx, items, defaultColor, nodeRadius, line
|
|
|
158
185
|
w: pxToIn(descLabelW),
|
|
159
186
|
h: pxToIn(descLabelH),
|
|
160
187
|
fontSize: pxToPt(11 * scaleFactor),
|
|
161
|
-
fontFace
|
|
188
|
+
fontFace,
|
|
162
189
|
color: textColors.description,
|
|
163
190
|
align: "left",
|
|
164
191
|
valign: "top"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeline.js","names":[],"sources":["../../../src/renderPptx/nodes/timeline.ts"],"sourcesContent":["import type { PositionedNode } from \"../../types.ts\";\nimport type { RenderContext } from \"../types.ts\";\nimport { stripHash } from \"../utils/visualStyle.ts\";\nimport { pxToIn, pxToPt } from \"../units.ts\";\nimport { measureTimeline } from \"../../calcYogaLayout/measureCompositeNodes.ts\";\nimport { resolveScaledContentArea } from \"../utils/scaleToFit.ts\";\nimport { withContentBounds } from \"../utils/contentArea.ts\";\n\ntype TimelinePositionedNode = Extract<PositionedNode, { type: \"timeline\" }>;\n\ntype TimelineTextColors = {\n date: string;\n title: string;\n description: string;\n};\n\nexport function renderTimelineNode(\n node: TimelinePositionedNode,\n ctx: RenderContext,\n): void {\n const direction = node.direction ?? \"horizontal\";\n const items = node.items;\n const itemCount = items.length;\n\n if (itemCount === 0) return;\n\n const defaultColor = \"1D4ED8\"; // blue\n const baseNodeRadius = 12; // px\n const baseLineWidth = 4; // px\n\n const textColors: TimelineTextColors = {\n date: stripHash(node.dateColor) ?? \"64748B\",\n title: stripHash(node.titleColor) ?? \"1E293B\",\n description: stripHash(node.descriptionColor) ?? \"64748B\",\n };\n\n // スケール係数を計算(コンテンツ領域基準)\n const { content, scaleFactor } = resolveScaledContentArea(\n node,\n measureTimeline(node),\n ctx,\n );\n\n const nodeRadius = baseNodeRadius * scaleFactor;\n const lineWidth = baseLineWidth * scaleFactor;\n\n // コンテンツ領域を使用するための仮想ノードを作成\n const contentNode = withContentBounds(node, content);\n\n if (direction === \"horizontal\") {\n renderHorizontalTimeline(\n contentNode,\n ctx,\n items,\n defaultColor,\n nodeRadius,\n lineWidth,\n scaleFactor,\n textColors,\n );\n } else {\n renderVerticalTimeline(\n contentNode,\n ctx,\n items,\n defaultColor,\n nodeRadius,\n lineWidth,\n scaleFactor,\n textColors,\n );\n }\n}\n\nfunction renderHorizontalTimeline(\n node: TimelinePositionedNode,\n ctx: RenderContext,\n items: TimelinePositionedNode[\"items\"],\n defaultColor: string,\n nodeRadius: number,\n lineWidth: number,\n scaleFactor: number,\n textColors: TimelineTextColors,\n): void {\n const itemCount = items.length;\n const lineY = node.y + node.h / 2;\n const labelW = 120 * scaleFactor;\n // 極端に狭い node.w でも startX <= endX を保つため、インセットを node.w/2 で頭打ちする\n const inset = Math.min(labelW / 2, node.w / 2);\n const startX = node.x + inset;\n const endX = node.x + node.w - inset;\n const lineLength = endX - startX;\n\n // メインの線を描画\n ctx.slide.addShape(ctx.pptx.ShapeType.line, {\n x: pxToIn(startX),\n y: pxToIn(lineY),\n w: pxToIn(lineLength),\n h: 0,\n line: { color: \"E2E8F0\", width: pxToPt(lineWidth) },\n });\n const dateLabelH = 24 * scaleFactor;\n const titleLabelH = 24 * scaleFactor;\n const descLabelH = 32 * scaleFactor;\n const dateOffset = 40 * scaleFactor;\n const titleGap = 8 * scaleFactor;\n const descOffset = 32 * scaleFactor;\n\n // 各アイテムを描画\n items.forEach((item, index) => {\n const progress = itemCount === 1 ? 0.5 : index / (itemCount - 1);\n const cx = startX + lineLength * progress;\n const cy = lineY;\n const color = item.color ?? defaultColor;\n\n // ノード(円)を描画\n ctx.slide.addShape(ctx.pptx.ShapeType.ellipse, {\n x: pxToIn(cx - nodeRadius),\n y: pxToIn(cy - nodeRadius),\n w: pxToIn(nodeRadius * 2),\n h: pxToIn(nodeRadius * 2),\n fill: { color },\n line: { type: \"none\" as const },\n });\n\n // 日付を上に表示\n ctx.slide.addText(item.date, {\n x: pxToIn(cx - labelW / 2),\n y: pxToIn(cy - nodeRadius - dateOffset),\n w: pxToIn(labelW),\n h: pxToIn(dateLabelH),\n fontSize: pxToPt(12 * scaleFactor),\n fontFace: \"Noto Sans JP\",\n color: textColors.date,\n align: \"center\",\n valign: \"bottom\",\n });\n\n // タイトルを下に表示\n ctx.slide.addText(item.title, {\n x: pxToIn(cx - labelW / 2),\n y: pxToIn(cy + nodeRadius + titleGap),\n w: pxToIn(labelW),\n h: pxToIn(titleLabelH),\n fontSize: pxToPt(14 * scaleFactor),\n fontFace: \"Noto Sans JP\",\n color: textColors.title,\n bold: true,\n align: \"center\",\n valign: \"top\",\n });\n\n // 説明を表示\n if (item.description) {\n ctx.slide.addText(item.description, {\n x: pxToIn(cx - labelW / 2),\n y: pxToIn(cy + nodeRadius + descOffset),\n w: pxToIn(labelW),\n h: pxToIn(descLabelH),\n fontSize: pxToPt(11 * scaleFactor),\n fontFace: \"Noto Sans JP\",\n color: textColors.description,\n align: \"center\",\n valign: \"top\",\n });\n }\n });\n}\n\nfunction renderVerticalTimeline(\n node: TimelinePositionedNode,\n ctx: RenderContext,\n items: TimelinePositionedNode[\"items\"],\n defaultColor: string,\n nodeRadius: number,\n lineWidth: number,\n scaleFactor: number,\n textColors: TimelineTextColors,\n): void {\n const itemCount = items.length;\n const lineX = node.x + 40 * scaleFactor;\n const startY = node.y + nodeRadius;\n const endY = node.y + node.h - nodeRadius;\n const lineLength = endY - startY;\n\n // メインの線を描画\n ctx.slide.addShape(ctx.pptx.ShapeType.line, {\n x: pxToIn(lineX),\n y: pxToIn(startY),\n w: 0,\n h: pxToIn(lineLength),\n line: { color: \"E2E8F0\", width: pxToPt(lineWidth) },\n });\n\n const labelGap = 16 * scaleFactor;\n const dateLabelW = 100 * scaleFactor;\n const dateLabelH = 20 * scaleFactor;\n const titleLabelH = 24 * scaleFactor;\n const descLabelH = 32 * scaleFactor;\n const titleLabelW = node.w - 80 * scaleFactor;\n const descLabelW = node.w - 80 * scaleFactor;\n\n // 各アイテムを描画\n items.forEach((item, index) => {\n const progress = itemCount === 1 ? 0.5 : index / (itemCount - 1);\n const cx = lineX;\n const cy = startY + lineLength * progress;\n const color = item.color ?? defaultColor;\n\n // ノード(円)を描画\n ctx.slide.addShape(ctx.pptx.ShapeType.ellipse, {\n x: pxToIn(cx - nodeRadius),\n y: pxToIn(cy - nodeRadius),\n w: pxToIn(nodeRadius * 2),\n h: pxToIn(nodeRadius * 2),\n fill: { color },\n line: { type: \"none\" as const },\n });\n\n // 日付を左上に表示\n ctx.slide.addText(item.date, {\n x: pxToIn(cx + nodeRadius + labelGap),\n y: pxToIn(cy - nodeRadius - 4 * scaleFactor),\n w: pxToIn(dateLabelW),\n h: pxToIn(dateLabelH),\n fontSize: pxToPt(12 * scaleFactor),\n fontFace: \"Noto Sans JP\",\n color: textColors.date,\n align: \"left\",\n valign: \"bottom\",\n });\n\n // タイトルを右に表示\n ctx.slide.addText(item.title, {\n x: pxToIn(cx + nodeRadius + labelGap),\n y: pxToIn(cy - 4 * scaleFactor),\n w: pxToIn(titleLabelW),\n h: pxToIn(titleLabelH),\n fontSize: pxToPt(14 * scaleFactor),\n fontFace: \"Noto Sans JP\",\n color: textColors.title,\n bold: true,\n align: \"left\",\n valign: \"top\",\n });\n\n // 説明を表示\n if (item.description) {\n ctx.slide.addText(item.description, {\n x: pxToIn(cx + nodeRadius + labelGap),\n y: pxToIn(cy + 20 * scaleFactor),\n w: pxToIn(descLabelW),\n h: pxToIn(descLabelH),\n fontSize: pxToPt(11 * scaleFactor),\n fontFace: \"Noto Sans JP\",\n color: textColors.description,\n align: \"left\",\n valign: \"top\",\n });\n }\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"timeline.js","names":[],"sources":["../../../src/renderPptx/nodes/timeline.ts"],"sourcesContent":["import type { PositionedNode } from \"../../types.ts\";\nimport type { RenderContext } from \"../types.ts\";\nimport { stripHash } from \"../utils/visualStyle.ts\";\nimport { pxToIn, pxToPt } from \"../units.ts\";\nimport { measureTimeline } from \"../../calcYogaLayout/measureCompositeNodes.ts\";\nimport { resolveScaledContentArea } from \"../utils/scaleToFit.ts\";\nimport { withContentBounds } from \"../utils/contentArea.ts\";\nimport { registerBackgroundGradient } from \"../gradientFills.ts\";\n\ntype TimelinePositionedNode = Extract<PositionedNode, { type: \"timeline\" }>;\n\ntype TimelineTextColors = {\n date: string;\n title: string;\n description: string;\n};\n\ntype TimelineRenderOptions = {\n defaultColor: string;\n nodeRadius: number;\n lineWidth: number;\n scaleFactor: number;\n textColors: TimelineTextColors;\n connectorLineColor: string;\n fontFace: string;\n useColorForDate: boolean;\n};\n\nexport function renderTimelineNode(\n node: TimelinePositionedNode,\n ctx: RenderContext,\n): void {\n const direction = node.direction ?? \"horizontal\";\n const items = node.items;\n const itemCount = items.length;\n\n if (itemCount === 0) return;\n\n const defaultColor = \"1D4ED8\"; // blue\n const baseNodeRadius = 12; // px\n const baseLineWidth = 4; // px\n\n const textColors: TimelineTextColors = {\n date: stripHash(node.dateColor) ?? \"64748B\",\n title: stripHash(node.titleColor) ?? \"1E293B\",\n description: stripHash(node.descriptionColor) ?? \"64748B\",\n };\n\n // 軸線色を解決する。connectorGradient が指定されていれば gradient registry に\n // 登録してマーカー色を採用する (後処理で gradFill に置換される)。\n // connectorColor が指定されていればそれを採用、未指定ならデフォルトの E2E8F0。\n const connectorGradientMarker = node.connectorGradient\n ? registerBackgroundGradient(\n node.connectorGradient,\n node.opacity,\n ctx.buildContext.gradientFills,\n )\n : undefined;\n const connectorLineColor =\n connectorGradientMarker ?? stripHash(node.connectorColor) ?? \"E2E8F0\";\n\n const fontFace = node.fontFamily ?? \"Noto Sans JP\";\n const useColorForDate = node.useColorForDate ?? false;\n\n // スケール係数を計算(コンテンツ領域基準)\n const { content, scaleFactor } = resolveScaledContentArea(\n node,\n measureTimeline(node),\n ctx,\n );\n\n const nodeRadius = baseNodeRadius * scaleFactor;\n const lineWidth = baseLineWidth * scaleFactor;\n\n // コンテンツ領域を使用するための仮想ノードを作成\n const contentNode = withContentBounds(node, content);\n\n const options: TimelineRenderOptions = {\n defaultColor,\n nodeRadius,\n lineWidth,\n scaleFactor,\n textColors,\n connectorLineColor,\n fontFace,\n useColorForDate,\n };\n\n if (direction === \"horizontal\") {\n renderHorizontalTimeline(contentNode, ctx, items, options);\n } else {\n renderVerticalTimeline(contentNode, ctx, items, options);\n }\n}\n\nfunction resolveItemDateColor(\n item: TimelinePositionedNode[\"items\"][number],\n options: TimelineRenderOptions,\n): string {\n // 優先順位: item.dateColor > (useColorForDate && item.color) > Timeline.dateColor\n const perItemDateColor = stripHash(item.dateColor);\n if (perItemDateColor) return perItemDateColor;\n if (options.useColorForDate && item.color) {\n const inherited = stripHash(item.color);\n if (inherited) return inherited;\n }\n return options.textColors.date;\n}\n\nfunction renderHorizontalTimeline(\n node: TimelinePositionedNode,\n ctx: RenderContext,\n items: TimelinePositionedNode[\"items\"],\n options: TimelineRenderOptions,\n): void {\n const {\n defaultColor,\n nodeRadius,\n lineWidth,\n scaleFactor,\n textColors,\n connectorLineColor,\n fontFace,\n } = options;\n const itemCount = items.length;\n const lineY = node.y + node.h / 2;\n const labelW = 120 * scaleFactor;\n // 極端に狭い node.w でも startX <= endX を保つため、インセットを node.w/2 で頭打ちする\n const inset = Math.min(labelW / 2, node.w / 2);\n const startX = node.x + inset;\n const endX = node.x + node.w - inset;\n const lineLength = endX - startX;\n\n // メインの線を描画\n ctx.slide.addShape(ctx.pptx.ShapeType.line, {\n x: pxToIn(startX),\n y: pxToIn(lineY),\n w: pxToIn(lineLength),\n h: 0,\n line: { color: connectorLineColor, width: pxToPt(lineWidth) },\n });\n const dateLabelH = 24 * scaleFactor;\n const titleLabelH = 24 * scaleFactor;\n const descLabelH = 32 * scaleFactor;\n const dateOffset = 40 * scaleFactor;\n const titleGap = 8 * scaleFactor;\n const descOffset = 32 * scaleFactor;\n\n // 各アイテムを描画\n items.forEach((item, index) => {\n const progress = itemCount === 1 ? 0.5 : index / (itemCount - 1);\n const cx = startX + lineLength * progress;\n const cy = lineY;\n const color = item.color ?? defaultColor;\n const dateColor = resolveItemDateColor(item, options);\n\n // ノード(円)を描画\n ctx.slide.addShape(ctx.pptx.ShapeType.ellipse, {\n x: pxToIn(cx - nodeRadius),\n y: pxToIn(cy - nodeRadius),\n w: pxToIn(nodeRadius * 2),\n h: pxToIn(nodeRadius * 2),\n fill: { color },\n line: { type: \"none\" as const },\n });\n\n // 日付を上に表示\n ctx.slide.addText(item.date, {\n x: pxToIn(cx - labelW / 2),\n y: pxToIn(cy - nodeRadius - dateOffset),\n w: pxToIn(labelW),\n h: pxToIn(dateLabelH),\n fontSize: pxToPt(12 * scaleFactor),\n fontFace,\n color: dateColor,\n align: \"center\",\n valign: \"bottom\",\n });\n\n // タイトルを下に表示\n ctx.slide.addText(item.title, {\n x: pxToIn(cx - labelW / 2),\n y: pxToIn(cy + nodeRadius + titleGap),\n w: pxToIn(labelW),\n h: pxToIn(titleLabelH),\n fontSize: pxToPt(14 * scaleFactor),\n fontFace,\n color: textColors.title,\n bold: true,\n align: \"center\",\n valign: \"top\",\n });\n\n // 説明を表示\n if (item.description) {\n ctx.slide.addText(item.description, {\n x: pxToIn(cx - labelW / 2),\n y: pxToIn(cy + nodeRadius + descOffset),\n w: pxToIn(labelW),\n h: pxToIn(descLabelH),\n fontSize: pxToPt(11 * scaleFactor),\n fontFace,\n color: textColors.description,\n align: \"center\",\n valign: \"top\",\n });\n }\n });\n}\n\nfunction renderVerticalTimeline(\n node: TimelinePositionedNode,\n ctx: RenderContext,\n items: TimelinePositionedNode[\"items\"],\n options: TimelineRenderOptions,\n): void {\n const {\n defaultColor,\n nodeRadius,\n lineWidth,\n scaleFactor,\n textColors,\n connectorLineColor,\n fontFace,\n } = options;\n const itemCount = items.length;\n const lineX = node.x + 40 * scaleFactor;\n const startY = node.y + nodeRadius;\n const endY = node.y + node.h - nodeRadius;\n const lineLength = endY - startY;\n\n // メインの線を描画\n ctx.slide.addShape(ctx.pptx.ShapeType.line, {\n x: pxToIn(lineX),\n y: pxToIn(startY),\n w: 0,\n h: pxToIn(lineLength),\n line: { color: connectorLineColor, width: pxToPt(lineWidth) },\n });\n\n const labelGap = 16 * scaleFactor;\n const dateLabelW = 100 * scaleFactor;\n const dateLabelH = 20 * scaleFactor;\n const titleLabelH = 24 * scaleFactor;\n const descLabelH = 32 * scaleFactor;\n const titleLabelW = node.w - 80 * scaleFactor;\n const descLabelW = node.w - 80 * scaleFactor;\n\n // 各アイテムを描画\n items.forEach((item, index) => {\n const progress = itemCount === 1 ? 0.5 : index / (itemCount - 1);\n const cx = lineX;\n const cy = startY + lineLength * progress;\n const color = item.color ?? defaultColor;\n const dateColor = resolveItemDateColor(item, options);\n\n // ノード(円)を描画\n ctx.slide.addShape(ctx.pptx.ShapeType.ellipse, {\n x: pxToIn(cx - nodeRadius),\n y: pxToIn(cy - nodeRadius),\n w: pxToIn(nodeRadius * 2),\n h: pxToIn(nodeRadius * 2),\n fill: { color },\n line: { type: \"none\" as const },\n });\n\n // 日付を左上に表示\n ctx.slide.addText(item.date, {\n x: pxToIn(cx + nodeRadius + labelGap),\n y: pxToIn(cy - nodeRadius - 4 * scaleFactor),\n w: pxToIn(dateLabelW),\n h: pxToIn(dateLabelH),\n fontSize: pxToPt(12 * scaleFactor),\n fontFace,\n color: dateColor,\n align: \"left\",\n valign: \"bottom\",\n });\n\n // タイトルを右に表示\n ctx.slide.addText(item.title, {\n x: pxToIn(cx + nodeRadius + labelGap),\n y: pxToIn(cy - 4 * scaleFactor),\n w: pxToIn(titleLabelW),\n h: pxToIn(titleLabelH),\n fontSize: pxToPt(14 * scaleFactor),\n fontFace,\n color: textColors.title,\n bold: true,\n align: \"left\",\n valign: \"top\",\n });\n\n // 説明を表示\n if (item.description) {\n ctx.slide.addText(item.description, {\n x: pxToIn(cx + nodeRadius + labelGap),\n y: pxToIn(cy + 20 * scaleFactor),\n w: pxToIn(descLabelW),\n h: pxToIn(descLabelH),\n fontSize: pxToPt(11 * scaleFactor),\n fontFace,\n color: textColors.description,\n align: \"left\",\n valign: \"top\",\n });\n }\n });\n}\n"],"mappings":"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"}
|
|
@@ -3,8 +3,8 @@ import { resolveBoxSpacing } from "../shared/boxSpacing.js";
|
|
|
3
3
|
import { getNodeDef } from "../registry/nodeRegistry.js";
|
|
4
4
|
import { pxToIn, pxToPt } from "./units.js";
|
|
5
5
|
import { convertStrike, convertUnderline } from "./textOptions.js";
|
|
6
|
-
import "../registry/index.js";
|
|
7
6
|
import { registerBackgroundGradient } from "./gradientFills.js";
|
|
7
|
+
import "../registry/index.js";
|
|
8
8
|
import { renderBackgroundAndBorder, renderBorderOnly } from "./utils/backgroundBorder.js";
|
|
9
9
|
//#region src/renderPptx/renderPptx.ts
|
|
10
10
|
async function loadPptxGenJS() {
|
package/dist/renderPptx/units.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
const EMU_PER_IN = 914400;
|
|
1
2
|
const pxToIn = (px) => px / 96;
|
|
2
3
|
const pxToPt = (px) => px * 72 / 96;
|
|
3
4
|
/**
|
|
5
|
+
* px を DrawingML の EMU (English Metric Unit) に変換する。
|
|
6
|
+
* 1 inch = 914400 EMU、96 DPI 基準で 1 px = 9525 EMU。
|
|
7
|
+
* `<a:glow rad="...">` など EMU を直接埋め込む XML 後処理で使う。
|
|
8
|
+
*/
|
|
9
|
+
const pxToEmu = (px) => px * EMU_PER_IN / 96;
|
|
10
|
+
/**
|
|
4
11
|
* px 単位の矩形を pptxgenjs の位置オプション (inch 単位の x/y/w/h) に
|
|
5
12
|
* まとめて変換する。addShape / addText 等のオプションへ spread して使う。
|
|
6
13
|
*/
|
|
@@ -11,6 +18,6 @@ const rectPxToIn = (rect) => ({
|
|
|
11
18
|
h: pxToIn(rect.h)
|
|
12
19
|
});
|
|
13
20
|
//#endregion
|
|
14
|
-
export { pxToIn, pxToPt, rectPxToIn };
|
|
21
|
+
export { pxToEmu, pxToIn, pxToPt, rectPxToIn };
|
|
15
22
|
|
|
16
23
|
//# sourceMappingURL=units.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"units.js","names":[],"sources":["../../src/renderPptx/units.ts"],"sourcesContent":["export const PX_PER_IN = 96;\n\nexport const pxToIn = (px: number) => px / PX_PER_IN;\n\nexport const pxToPt = (px: number) => (px * 72) / PX_PER_IN;\n\n/**\n * px 単位の矩形を pptxgenjs の位置オプション (inch 単位の x/y/w/h) に\n * まとめて変換する。addShape / addText 等のオプションへ spread して使う。\n */\nexport const rectPxToIn = (rect: {\n x: number;\n y: number;\n w: number;\n h: number;\n}) => ({\n x: pxToIn(rect.x),\n y: pxToIn(rect.y),\n w: pxToIn(rect.w),\n h: pxToIn(rect.h),\n});\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"units.js","names":[],"sources":["../../src/renderPptx/units.ts"],"sourcesContent":["export const PX_PER_IN = 96;\nexport const EMU_PER_IN = 914400;\n\nexport const pxToIn = (px: number) => px / PX_PER_IN;\n\nexport const pxToPt = (px: number) => (px * 72) / PX_PER_IN;\n\n/**\n * px を DrawingML の EMU (English Metric Unit) に変換する。\n * 1 inch = 914400 EMU、96 DPI 基準で 1 px = 9525 EMU。\n * `<a:glow rad=\"...\">` など EMU を直接埋め込む XML 後処理で使う。\n */\nexport const pxToEmu = (px: number) => (px * EMU_PER_IN) / PX_PER_IN;\n\n/**\n * px 単位の矩形を pptxgenjs の位置オプション (inch 単位の x/y/w/h) に\n * まとめて変換する。addShape / addText 等のオプションへ spread して使う。\n */\nexport const rectPxToIn = (rect: {\n x: number;\n y: number;\n w: number;\n h: number;\n}) => ({\n x: pxToIn(rect.x),\n y: pxToIn(rect.y),\n w: pxToIn(rect.w),\n h: pxToIn(rect.h),\n});\n"],"mappings":"AACA,MAAa,aAAa;AAE1B,MAAa,UAAU,OAAe,KAAA;AAEtC,MAAa,UAAU,OAAgB,KAAK,KAAA;;;;;;AAO5C,MAAa,WAAW,OAAgB,KAAK,aAAA;;;;;AAM7C,MAAa,cAAc,UAKpB;CACL,GAAG,OAAO,KAAK,CAAC;CAChB,GAAG,OAAO,KAAK,CAAC;CAChB,GAAG,OAAO,KAAK,CAAC;CAChB,GAAG,OAAO,KAAK,CAAC;AAClB"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getImageData } from "../../shared/measureImage.js";
|
|
2
2
|
import { pxToIn, rectPxToIn } from "../units.js";
|
|
3
|
-
import { BORDER_SIDES, convertBorderLine, convertShadow, hasVisibleBorder, resolveBackgroundFill, resolvePerSideBorders, resolveRectRadius } from "./visualStyle.js";
|
|
4
3
|
import { registerBackgroundGradient } from "../gradientFills.js";
|
|
4
|
+
import { BORDER_SIDES, convertBorderLine, convertShadow, hasVisibleBorder, resolveBackgroundFill, resolvePerSideBorders, resolveRectRadius } from "./visualStyle.js";
|
|
5
5
|
//#region src/renderPptx/utils/backgroundBorder.ts
|
|
6
6
|
/**
|
|
7
7
|
* ノードの背景色・背景画像・ボーダー・影を描画する
|