|
@@ -1,495 +1,511 @@
|
|
|
import { nanoid } from "nanoid"
|
|
import { nanoid } from "nanoid"
|
|
|
-import type { Slide, PPTElement, SlideTheme, PPTTextElement, PPTImageElement, PPTShapeElement, SlideBackground, PPTElementShadow } from "@/types/slides"
|
|
|
|
|
|
|
+import type {
|
|
|
|
|
+ Gradient,
|
|
|
|
|
+ PPTElement,
|
|
|
|
|
+ PPTElementShadow,
|
|
|
|
|
+ PPTImageElement,
|
|
|
|
|
+ PPTLineElement,
|
|
|
|
|
+ PPTShapeElement,
|
|
|
|
|
+ PPTTextElement,
|
|
|
|
|
+ Slide,
|
|
|
|
|
+ SlideBackground,
|
|
|
|
|
+ SlideTheme,
|
|
|
|
|
+} from "@/types/slides"
|
|
|
import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
|
|
import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
|
|
|
|
|
|
|
|
interface AiPptOptions {
|
|
interface AiPptOptions {
|
|
|
backgroundImage?: string
|
|
backgroundImage?: string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+type PageRole = "cover" | "intro" | "goal" | "story" | "listen" | "lyric" | "knowledge" | "rhythm" | "interaction" | "activity" | "summary" | "homework" | "content"
|
|
|
|
|
+
|
|
|
interface Palette {
|
|
interface Palette {
|
|
|
primary: string
|
|
primary: string
|
|
|
secondary: string
|
|
secondary: string
|
|
|
accent: string
|
|
accent: string
|
|
|
warm: string
|
|
warm: string
|
|
|
background: string
|
|
background: string
|
|
|
- panel: string
|
|
|
|
|
- panelAlt: string
|
|
|
|
|
|
|
+ surface: string
|
|
|
|
|
+ soft: string
|
|
|
text: string
|
|
text: string
|
|
|
muted: string
|
|
muted: string
|
|
|
|
|
+ dark: string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-interface VisualRect {
|
|
|
|
|
|
|
+interface ImageRect {
|
|
|
left: number
|
|
left: number
|
|
|
top: number
|
|
top: number
|
|
|
width: number
|
|
width: number
|
|
|
height: number
|
|
height: number
|
|
|
|
|
+ radius?: number
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const SLIDE_WIDTH = 1600
|
|
const SLIDE_WIDTH = 1600
|
|
|
const SLIDE_HEIGHT = 900
|
|
const SLIDE_HEIGHT = 900
|
|
|
const FONT_NAME = "Microsoft Yahei"
|
|
const FONT_NAME = "Microsoft Yahei"
|
|
|
|
|
+const GENERIC_PAGE_TITLES = new Set(["封面", "课程导入", "故事导入", "导入", "首页", "cover"])
|
|
|
|
|
|
|
|
export function buildAiPptSlides(outline: AiCoursewareOutline, options: AiPptOptions = {}): { title: string; theme: Partial<SlideTheme>; slides: Slide[] } {
|
|
export function buildAiPptSlides(outline: AiCoursewareOutline, options: AiPptOptions = {}): { title: string; theme: Partial<SlideTheme>; slides: Slide[] } {
|
|
|
- const palette = buildPalette(outline)
|
|
|
|
|
|
|
+ const normalized = normalizeOutline(outline)
|
|
|
|
|
+ const palette = buildPalette(normalized)
|
|
|
|
|
+ const slides = normalized.pages.map((page, index) => buildSlide(page, normalized, palette, options, index))
|
|
|
|
|
+ runPreflight(slides)
|
|
|
return {
|
|
return {
|
|
|
- title: outline.title || outline.chapterName || "音乐课件",
|
|
|
|
|
|
|
+ title: normalized.title || normalized.chapterName || "音乐课件",
|
|
|
theme: {
|
|
theme: {
|
|
|
themeColor: palette.primary,
|
|
themeColor: palette.primary,
|
|
|
backgroundColor: palette.background,
|
|
backgroundColor: palette.background,
|
|
|
fontColor: palette.text,
|
|
fontColor: palette.text,
|
|
|
fontName: FONT_NAME,
|
|
fontName: FONT_NAME,
|
|
|
- fontSize: "36px"
|
|
|
|
|
|
|
+ fontSize: "36px",
|
|
|
},
|
|
},
|
|
|
- slides: outline.pages.map(page => buildSlide(page, outline, palette, options))
|
|
|
|
|
|
|
+ slides,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, options: AiPptOptions): Slide {
|
|
|
|
|
- const elements = baseElements(palette, options)
|
|
|
|
|
- const kind = pageKind(page)
|
|
|
|
|
-
|
|
|
|
|
- if (page.pageNo === 1 || kind.includes("cover")) {
|
|
|
|
|
- elements.push(...coverTemplate(page, outline, palette))
|
|
|
|
|
- } else if (kind.includes("goal")) {
|
|
|
|
|
- elements.push(...cardsTemplate(page, outline, palette, "学习目标"))
|
|
|
|
|
- } else if (kind.includes("listen")) {
|
|
|
|
|
- elements.push(...listenTemplate(page, outline, palette))
|
|
|
|
|
- } else if (kind.includes("lyric") || kind.includes("content")) {
|
|
|
|
|
- elements.push(...contentTemplate(page, outline, palette))
|
|
|
|
|
- } else if (kind.includes("rhythm") || kind.includes("skill")) {
|
|
|
|
|
- elements.push(...rhythmTemplate(page, outline, palette))
|
|
|
|
|
- } else if (kind.includes("knowledge") || kind.includes("extension")) {
|
|
|
|
|
- elements.push(...cardsTemplate(page, outline, palette, "音乐知识"))
|
|
|
|
|
- } else if (kind.includes("interaction") || kind.includes("activity")) {
|
|
|
|
|
- elements.push(...activityTemplate(page, outline, palette))
|
|
|
|
|
- } else if (kind.includes("summary")) {
|
|
|
|
|
- elements.push(...summaryTemplate(page, outline, palette))
|
|
|
|
|
- } else if (kind.includes("homework")) {
|
|
|
|
|
- elements.push(...homeworkTemplate(page, outline, palette))
|
|
|
|
|
- } else {
|
|
|
|
|
- elements.push(...splitTemplate(page, outline, palette))
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, options: AiPptOptions, index: number): Slide {
|
|
|
|
|
+ const role = pageRole(page, index)
|
|
|
|
|
+ const elements = templateFor(role)(page, outline, palette, index)
|
|
|
return {
|
|
return {
|
|
|
id: nanoid(10),
|
|
id: nanoid(10),
|
|
|
elements: keepInCanvas(elements),
|
|
elements: keepInCanvas(elements),
|
|
|
- background: backgroundConfig(palette, options),
|
|
|
|
|
- remark: slideRemark(page)
|
|
|
|
|
|
|
+ background: backgroundConfig(role, palette, options),
|
|
|
|
|
+ remark: slideRemark(page),
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function templateFor(role: PageRole) {
|
|
|
|
|
+ switch (role) {
|
|
|
|
|
+ case "cover": return coverTemplate
|
|
|
|
|
+ case "goal": return goalTemplate
|
|
|
|
|
+ case "listen": return listenTemplate
|
|
|
|
|
+ case "lyric": return lyricTemplate
|
|
|
|
|
+ case "rhythm": return rhythmTemplate
|
|
|
|
|
+ case "knowledge": return knowledgeTemplate
|
|
|
|
|
+ case "interaction": return interactionTemplate
|
|
|
|
|
+ case "activity": return activityTemplate
|
|
|
|
|
+ case "summary": return summaryTemplate
|
|
|
|
|
+ case "homework": return homeworkTemplate
|
|
|
|
|
+ case "intro":
|
|
|
|
|
+ case "story": return storyTemplate
|
|
|
|
|
+ default: return contentTemplate
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function coverTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
function coverTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
|
|
+ const title = chapterTitle(outline, page)
|
|
|
|
|
+ const subtitle = [outline.gradeName, conciseTextbook(outline.textbookName), outline.unitName].filter(Boolean).join(" · ")
|
|
|
|
|
+ const points = studentPoints(page, 2)
|
|
|
|
|
+ if (hasImage(page)) {
|
|
|
|
|
+ const elements: PPTElement[] = [imageElement(page, { left: 0, top: 0, width: SLIDE_WIDTH, height: SLIDE_HEIGHT })]
|
|
|
|
|
+ elements.push(shapeElement({ left: 0, top: 0, width: 910, height: 900, fill: palette.dark, opacity: 0.78, radius: 0 }))
|
|
|
|
|
+ elements.push(textElement({ left: 100, top: 150, width: 700, height: 150, content: title, fontSize: fitFont(title, 700, 150, 66, 48), color: "#FFFFFF", bold: true }))
|
|
|
|
|
+ if (subtitle) elements.push(textElement({ left: 104, top: 330, width: 680, height: 48, content: subtitle, fontSize: 24, color: "#EAF2F7" }))
|
|
|
|
|
+ elements.push(shapeElement({ left: 104, top: 430, width: 84, height: 10, fill: palette.accent, radius: 4 }))
|
|
|
|
|
+ points.forEach((point, index) => elements.push(textElement({ left: 104, top: 500 + index * 64, width: 680, height: 44, content: point, fontSize: fitFont(point, 680, 44, 28, 22), color: "#FFFFFF", bold: index === 0 })))
|
|
|
|
|
+ return elements
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const elements: PPTElement[] = []
|
|
const elements: PPTElement[] = []
|
|
|
- const subtitle = [outline.gradeName, outline.textbookName, outline.unitName].filter(Boolean).join(" · ")
|
|
|
|
|
- elements.push(titleMark(92, 92, palette.primary))
|
|
|
|
|
- elements.push(textElement({ left: 92, top: 138, width: 780, height: 100, content: pageTitle(page, outline), fontSize: 54, color: palette.primary, bold: true }))
|
|
|
|
|
- if (subtitle) elements.push(textElement({ left: 96, top: 266, width: 780, height: 34, content: subtitle, fontSize: 22, color: palette.text }))
|
|
|
|
|
- elements.push(...heroVisual(page, palette, { left: 940, top: 122, width: 470, height: 420 }))
|
|
|
|
|
- elements.push(...compactBullets(studentPoints(page, 2), palette, 96, 390, 660, "♪"))
|
|
|
|
|
- addTeacherAction(elements, page, palette, 96, 715, 900)
|
|
|
|
|
|
|
+ elements.push(shapeElement({ left: 0, top: 0, width: 560, height: 900, fill: palette.primary, radius: 0 }))
|
|
|
|
|
+ elements.push(shapeElement({ left: 560, top: 0, width: 1040, height: 900, fill: palette.background, radius: 0 }))
|
|
|
|
|
+ elements.push(textElement({ left: 92, top: 112, width: 380, height: 70, content: "音乐课堂", fontSize: 30, color: "#FFFFFF", bold: true }))
|
|
|
|
|
+ elements.push(textElement({ left: 650, top: 170, width: 780, height: 170, content: title, fontSize: fitFont(title, 780, 170, 70, 50), color: palette.primary, bold: true }))
|
|
|
|
|
+ if (subtitle) elements.push(textElement({ left: 656, top: 370, width: 740, height: 44, content: subtitle, fontSize: 24, color: palette.muted }))
|
|
|
|
|
+ elements.push(...melodyStaff(120, 350, 360, palette))
|
|
|
|
|
+ elements.push(shapeElement({ left: 654, top: 486, width: 120, height: 10, fill: palette.secondary, radius: 4 }))
|
|
|
|
|
+ points.forEach((point, index) => elements.push(textElement({ left: 656, top: 548 + index * 66, width: 720, height: 46, content: point, fontSize: fitFont(point, 720, 46, 30, 23), color: palette.text, bold: index === 0 })))
|
|
|
return elements
|
|
return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function splitTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
|
|
- const elements: PPTElement[] = []
|
|
|
|
|
- addSlideTitle(elements, page, outline, palette)
|
|
|
|
|
- elements.push(...visualBlock(page, palette, { left: 925, top: 178, width: 500, height: 430 }))
|
|
|
|
|
- elements.push(...largeStudentPanel(page, palette, 96, 188, 700, 440))
|
|
|
|
|
- addTeacherAction(elements, page, palette, 96, 692, 1190)
|
|
|
|
|
|
|
+function storyTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, index: number): PPTElement[] {
|
|
|
|
|
+ const elements = slideHeader(page, outline, palette, "进入情境")
|
|
|
|
|
+ const imageLeft = index % 2 === 0
|
|
|
|
|
+ if (hasImage(page)) {
|
|
|
|
|
+ const imageRect = imageLeft ? { left: 86, top: 190, width: 860, height: 560, radius: 8 } : { left: 654, top: 190, width: 860, height: 560, radius: 8 }
|
|
|
|
|
+ elements.push(imageElement(page, imageRect))
|
|
|
|
|
+ const panelLeft = imageLeft ? 1010 : 86
|
|
|
|
|
+ elements.push(shapeElement({ left: panelLeft, top: 230, width: 500, height: 470, fill: palette.surface, radius: 8, shadow: softShadow() }))
|
|
|
|
|
+ elements.push(...studentPanel(page, palette, panelLeft + 42, 278, 416))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ elements.push(shapeElement({ left: 86, top: 210, width: 1428, height: 480, fill: palette.surface, radius: 8, shadow: softShadow() }))
|
|
|
|
|
+ elements.push(textElement({ left: 156, top: 270, width: 1280, height: 90, content: studentHeadline(page) || page.title, fontSize: fitFont(studentHeadline(page) || page.title, 1280, 90, 42, 30), color: palette.primary, bold: true }))
|
|
|
|
|
+ elements.push(...storySequence(studentPoints(page, 4), palette, 156, 420, 1280))
|
|
|
|
|
+ }
|
|
|
|
|
+ addClassroomCue(elements, page, palette)
|
|
|
return elements
|
|
return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function contentTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
|
|
- const elements: PPTElement[] = []
|
|
|
|
|
- addSlideTitle(elements, page, outline, palette)
|
|
|
|
|
- elements.push(shapeElement({ left: 96, top: 184, width: 720, height: 460, fill: palette.panel, radius: 24, shadow: softShadow() }))
|
|
|
|
|
- elements.push(textElement({ left: 136, top: 226, width: 640, height: 54, content: studentHeadline(page) || page.title, fontSize: 34, color: palette.primary, bold: true }))
|
|
|
|
|
- studentPoints(page, 4).forEach((point, index) => {
|
|
|
|
|
- const top = 318 + index * 72
|
|
|
|
|
- elements.push(shapeElement({ left: 138, top, width: 24, height: 24, fill: index % 2 ? palette.secondary : palette.primary, radius: 6 }))
|
|
|
|
|
- elements.push(textElement({ left: 186, top: top - 4, width: 560, height: 42, content: point, fontSize: 28, color: palette.text }))
|
|
|
|
|
|
|
+function goalTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
|
|
+ const elements = slideHeader(page, outline, palette, "本课目标")
|
|
|
|
|
+ const points = studentPoints(page, 3)
|
|
|
|
|
+ points.forEach((point, index) => {
|
|
|
|
|
+ const left = 92 + index * 500
|
|
|
|
|
+ const colors = [palette.primary, palette.secondary, palette.accent]
|
|
|
|
|
+ elements.push(shapeElement({ left, top: 240, width: 430, height: 380, fill: palette.surface, radius: 8, shadow: softShadow() }))
|
|
|
|
|
+ elements.push(shapeElement({ left, top: 240, width: 430, height: 18, fill: colors[index], radius: 0 }))
|
|
|
|
|
+ elements.push(textElement({ left: left + 34, top: 300, width: 96, height: 74, content: `0${index + 1}`, fontSize: 50, color: colors[index], bold: true }))
|
|
|
|
|
+ elements.push(textElement({ left: left + 34, top: 410, width: 350, height: 120, content: point, fontSize: fitFont(point, 350, 120, 32, 25), color: palette.text, bold: true, lineHeight: 1.35 }))
|
|
|
})
|
|
})
|
|
|
- elements.push(...visualBlock(page, palette, { left: 900, top: 204, width: 510, height: 410 }))
|
|
|
|
|
- addTeacherAction(elements, page, palette, 110, 700, 1180)
|
|
|
|
|
|
|
+ addClassroomCue(elements, page, palette)
|
|
|
return elements
|
|
return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function listenTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
function listenTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
- const elements: PPTElement[] = []
|
|
|
|
|
- addSlideTitle(elements, page, outline, palette)
|
|
|
|
|
- const task = activityTask(page) || studentHeadline(page) || "听一听:音乐中有哪些声音?"
|
|
|
|
|
- elements.push(shapeElement({ left: 96, top: 184, width: 760, height: 150, fill: palette.primary, radius: 26, shadow: softShadow() }))
|
|
|
|
|
- elements.push(textElement({ left: 142, top: 226, width: 660, height: 60, content: task, fontSize: 34, color: "#FFFFFF", bold: true, lineHeight: 1.25 }))
|
|
|
|
|
- elements.push(...soundWave(136, 390, palette, 610, 115))
|
|
|
|
|
- elements.push(...compactBullets(studentPoints(page, 3), palette, 104, 552, 690, "听"))
|
|
|
|
|
- elements.push(...visualBlock(page, palette, { left: 925, top: 190, width: 490, height: 430 }))
|
|
|
|
|
- addTeacherAction(elements, page, palette, 110, 724, 1180)
|
|
|
|
|
|
|
+ const elements = slideHeader(page, outline, palette, "听赏任务")
|
|
|
|
|
+ const task = activityTask(page) || studentHeadline(page) || "安静聆听,找一找音乐中的声音"
|
|
|
|
|
+ if (hasImage(page)) {
|
|
|
|
|
+ elements.push(imageElement(page, { left: 84, top: 185, width: 1432, height: 520, radius: 8 }))
|
|
|
|
|
+ elements.push(shapeElement({ left: 84, top: 510, width: 1432, height: 195, fill: palette.dark, opacity: 0.8, radius: 0 }))
|
|
|
|
|
+ elements.push(textElement({ left: 130, top: 550, width: 1340, height: 84, content: task, fontSize: fitFont(task, 1340, 84, 42, 30), color: "#FFFFFF", bold: true }))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ elements.push(shapeElement({ left: 92, top: 210, width: 1416, height: 220, fill: palette.dark, radius: 8 }))
|
|
|
|
|
+ elements.push(textElement({ left: 150, top: 270, width: 1300, height: 92, content: task, fontSize: fitFont(task, 1300, 92, 44, 30), color: "#FFFFFF", bold: true }))
|
|
|
|
|
+ elements.push(...soundWave(250, 520, palette, 1080, 150))
|
|
|
|
|
+ }
|
|
|
|
|
+ addClassroomCue(elements, page, palette)
|
|
|
|
|
+ return elements
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function lyricTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
|
|
+ const elements = slideHeader(page, outline, palette, "学唱与歌词")
|
|
|
|
|
+ const points = studentPoints(page, 4)
|
|
|
|
|
+ elements.push(shapeElement({ left: 90, top: 205, width: 1420, height: 470, fill: palette.surface, radius: 8, shadow: softShadow() }))
|
|
|
|
|
+ elements.push(shapeElement({ left: 90, top: 205, width: 26, height: 470, fill: palette.secondary, radius: 0 }))
|
|
|
|
|
+ points.forEach((point, index) => {
|
|
|
|
|
+ const top = 265 + index * 92
|
|
|
|
|
+ elements.push(textElement({ left: 166, top, width: 1250, height: 58, content: point, fontSize: fitFont(point, 1250, 58, 36, 27), color: index % 2 === 0 ? palette.text : palette.primary, bold: index % 2 === 1 }))
|
|
|
|
|
+ })
|
|
|
|
|
+ addClassroomCue(elements, page, palette)
|
|
|
return elements
|
|
return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function rhythmTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
function rhythmTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
- const elements: PPTElement[] = []
|
|
|
|
|
- addSlideTitle(elements, page, outline, palette)
|
|
|
|
|
|
|
+ const elements = slideHeader(page, outline, palette, "节奏练习")
|
|
|
const task = activityTask(page) || studentHeadline(page) || "拍一拍,感受节奏"
|
|
const task = activityTask(page) || studentHeadline(page) || "拍一拍,感受节奏"
|
|
|
- elements.push(shapeElement({ left: 105, top: 185, width: 570, height: 96, fill: palette.primary, radius: 22, shadow: softShadow() }))
|
|
|
|
|
- elements.push(textElement({ left: 144, top: 216, width: 500, height: 36, content: task, fontSize: 30, color: "#FFFFFF", bold: true }))
|
|
|
|
|
|
|
+ elements.push(textElement({ left: 92, top: 190, width: 1410, height: 72, content: task, fontSize: fitFont(task, 1410, 72, 40, 30), color: palette.text, bold: true }))
|
|
|
const beats = activitySteps(page, 4)
|
|
const beats = activitySteps(page, 4)
|
|
|
beats.forEach((beat, index) => {
|
|
beats.forEach((beat, index) => {
|
|
|
- const left = 108 + index * 205
|
|
|
|
|
- elements.push(shapeElement({ left, top: 365, width: 160, height: 160, fill: index % 2 ? palette.panelAlt : palette.panel, radius: 24, shadow: softShadow() }))
|
|
|
|
|
- elements.push(textElement({ left: left + 44, top: 400, width: 74, height: 48, content: `${index + 1}`, fontSize: 38, color: palette.primary, bold: true }))
|
|
|
|
|
- elements.push(textElement({ left: left + 22, top: 468, width: 116, height: 40, content: beat, fontSize: 20, color: palette.text, lineHeight: 1.25 }))
|
|
|
|
|
|
|
+ const left = 92 + index * 370
|
|
|
|
|
+ elements.push(shapeElement({ left, top: 330, width: 320, height: 260, fill: index % 2 === 0 ? palette.soft : palette.surface, radius: 8, shadow: lightShadow() }))
|
|
|
|
|
+ elements.push(textElement({ left: left + 28, top: 370, width: 70, height: 56, content: `${index + 1}`, fontSize: 42, color: index % 2 === 0 ? palette.primary : palette.secondary, bold: true }))
|
|
|
|
|
+ elements.push(textElement({ left: left + 28, top: 460, width: 260, height: 90, content: beat, fontSize: fitFont(beat, 260, 90, 29, 23), color: palette.text, bold: true, lineHeight: 1.3 }))
|
|
|
})
|
|
})
|
|
|
- elements.push(...visualBlock(page, palette, { left: 980, top: 230, width: 360, height: 360 }))
|
|
|
|
|
- addTeacherAction(elements, page, palette, 108, 704, 1190)
|
|
|
|
|
|
|
+ addClassroomCue(elements, page, palette)
|
|
|
return elements
|
|
return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function cardsTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, fallbackHeadline: string): PPTElement[] {
|
|
|
|
|
- const elements: PPTElement[] = []
|
|
|
|
|
- addSlideTitle(elements, page, outline, palette)
|
|
|
|
|
|
|
+function knowledgeTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, index: number): PPTElement[] {
|
|
|
|
|
+ const elements = slideHeader(page, outline, palette, "音乐知识")
|
|
|
const points = studentPoints(page, 4)
|
|
const points = studentPoints(page, 4)
|
|
|
- points.forEach((point, index) => {
|
|
|
|
|
- const left = 108 + (index % 2) * 520
|
|
|
|
|
- const top = 210 + Math.floor(index / 2) * 188
|
|
|
|
|
- elements.push(shapeElement({ left, top, width: 460, height: 136, fill: index % 2 ? palette.panelAlt : palette.panel, radius: 22, shadow: softShadow() }))
|
|
|
|
|
- elements.push(shapeElement({ left: left + 28, top: top + 34, width: 42, height: 42, fill: index % 2 ? palette.secondary : palette.primary, radius: 12 }))
|
|
|
|
|
- elements.push(textElement({ left: left + 42, top: top + 42, width: 18, height: 22, content: `${index + 1}`, fontSize: 20, color: "#FFFFFF", bold: true }))
|
|
|
|
|
- elements.push(textElement({ left: left + 92, top: top + 34, width: 320, height: 58, content: point, fontSize: 25, color: palette.text, bold: true, lineHeight: 1.3 }))
|
|
|
|
|
|
|
+ if (hasImage(page)) {
|
|
|
|
|
+ const imageRight = index % 2 === 0
|
|
|
|
|
+ elements.push(imageElement(page, imageRight ? { left: 900, top: 190, width: 610, height: 500, radius: 8 } : { left: 90, top: 190, width: 610, height: 500, radius: 8 }))
|
|
|
|
|
+ elements.push(...knowledgeList(points, palette, imageRight ? 90 : 770, 220, 720))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ elements.push(...knowledgeList(points, palette, 90, 220, 1420))
|
|
|
|
|
+ }
|
|
|
|
|
+ addClassroomCue(elements, page, palette)
|
|
|
|
|
+ return elements
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function interactionTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
|
|
+ const elements = slideHeader(page, outline, palette, "互动挑战")
|
|
|
|
|
+ const question = page.interaction?.question || activityTask(page) || studentHeadline(page) || "你发现了什么?"
|
|
|
|
|
+ elements.push(shapeElement({ left: 90, top: 200, width: 1420, height: 190, fill: palette.primary, radius: 8 }))
|
|
|
|
|
+ elements.push(textElement({ left: 150, top: 245, width: 1300, height: 100, content: question, fontSize: fitFont(question, 1300, 100, 44, 30), color: "#FFFFFF", bold: true }))
|
|
|
|
|
+ const steps = activitySteps(page, 3)
|
|
|
|
|
+ steps.forEach((step, index) => {
|
|
|
|
|
+ const left = 90 + index * 490
|
|
|
|
|
+ elements.push(textElement({ left, top: 468, width: 88, height: 54, content: `0${index + 1}`, fontSize: 40, color: [palette.primary, palette.secondary, palette.accent][index], bold: true }))
|
|
|
|
|
+ elements.push(textElement({ left: left + 96, top: 466, width: 340, height: 100, content: step, fontSize: fitFont(step, 340, 100, 29, 23), color: palette.text, bold: true, lineHeight: 1.3 }))
|
|
|
|
|
+ if (index < steps.length - 1) elements.push(lineElement(left + 452, 492, left + 478, 492, palette.muted))
|
|
|
})
|
|
})
|
|
|
- elements.push(...visualBlock(page, palette, { left: 1145, top: 248, width: 265, height: 265 }))
|
|
|
|
|
- addTeacherAction(elements, page, palette, 108, 706, 1180, activityTask(page) || fallbackHeadline)
|
|
|
|
|
|
|
+ addClassroomCue(elements, page, palette)
|
|
|
return elements
|
|
return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function activityTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
function activityTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
- const elements: PPTElement[] = []
|
|
|
|
|
- addSlideTitle(elements, page, outline, palette)
|
|
|
|
|
- const task = activityTask(page) || page.interaction?.question || studentHeadline(page) || "课堂互动"
|
|
|
|
|
- elements.push(shapeElement({ left: 108, top: 185, width: 900, height: 165, fill: palette.primary, radius: 28, shadow: softShadow() }))
|
|
|
|
|
- elements.push(textElement({ left: 154, top: 235, width: 800, height: 58, content: task, fontSize: 36, color: "#FFFFFF", bold: true, lineHeight: 1.25 }))
|
|
|
|
|
- activitySteps(page, 3).forEach((step, index) => {
|
|
|
|
|
- const left = 118 + index * 392
|
|
|
|
|
- elements.push(shapeElement({ left, top: 455, width: 335, height: 142, fill: palette.panel, radius: 22, shadow: softShadow() }))
|
|
|
|
|
- elements.push(textElement({ left: left + 26, top: 484, width: 88, height: 26, content: `步骤${index + 1}`, fontSize: 21, color: palette.primary, bold: true }))
|
|
|
|
|
- elements.push(textElement({ left: left + 26, top: 532, width: 280, height: 42, content: step, fontSize: 24, color: palette.text, lineHeight: 1.28 }))
|
|
|
|
|
- })
|
|
|
|
|
- elements.push(...visualBlock(page, palette, { left: 1095, top: 195, width: 300, height: 300 }))
|
|
|
|
|
- addTeacherAction(elements, page, palette, 116, 710, 1170, page.interaction?.answer || "")
|
|
|
|
|
|
|
+ const elements = slideHeader(page, outline, palette, "课堂活动")
|
|
|
|
|
+ const task = activityTask(page) || studentHeadline(page) || "一起完成课堂挑战"
|
|
|
|
|
+ elements.push(textElement({ left: 92, top: 190, width: 1400, height: 90, content: task, fontSize: fitFont(task, 1400, 90, 42, 30), color: palette.primary, bold: true }))
|
|
|
|
|
+ elements.push(...storySequence(activitySteps(page, 4), palette, 92, 360, 1416))
|
|
|
|
|
+ addClassroomCue(elements, page, palette)
|
|
|
return elements
|
|
return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function summaryTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
function summaryTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
- const elements: PPTElement[] = []
|
|
|
|
|
- addSlideTitle(elements, page, outline, palette)
|
|
|
|
|
- elements.push(shapeElement({ left: 112, top: 188, width: 900, height: 470, fill: palette.panel, radius: 26, shadow: softShadow() }))
|
|
|
|
|
|
|
+ const elements = slideHeader(page, outline, palette, "课堂小结")
|
|
|
studentPoints(page, 4).forEach((point, index) => {
|
|
studentPoints(page, 4).forEach((point, index) => {
|
|
|
- const top = 240 + index * 90
|
|
|
|
|
- elements.push(shapeElement({ left: 160, top, width: 44, height: 44, fill: palette.secondary, radius: 14 }))
|
|
|
|
|
- elements.push(textElement({ left: 172, top: top + 8, width: 20, height: 20, content: "✓", fontSize: 23, color: "#FFFFFF", bold: true }))
|
|
|
|
|
- elements.push(textElement({ left: 236, top: top + 4, width: 690, height: 38, content: point, fontSize: 28, color: palette.text, bold: true }))
|
|
|
|
|
|
|
+ const top = 220 + index * 112
|
|
|
|
|
+ const color = index % 2 === 0 ? palette.secondary : palette.primary
|
|
|
|
|
+ elements.push(shapeElement({ left: 100, top, width: 62, height: 62, fill: color, radius: 8 }))
|
|
|
|
|
+ elements.push(textElement({ left: 118, top: top + 12, width: 30, height: 30, content: "✓", fontSize: 28, color: "#FFFFFF", bold: true }))
|
|
|
|
|
+ elements.push(textElement({ left: 210, top: top + 4, width: 1240, height: 64, content: point, fontSize: fitFont(point, 1240, 64, 34, 26), color: palette.text, bold: true }))
|
|
|
})
|
|
})
|
|
|
- elements.push(...visualBlock(page, palette, { left: 1120, top: 250, width: 280, height: 280 }))
|
|
|
|
|
return elements
|
|
return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function homeworkTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
function homeworkTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
- const elements: PPTElement[] = []
|
|
|
|
|
- addSlideTitle(elements, page, outline, palette)
|
|
|
|
|
- elements.push(shapeElement({ left: 112, top: 196, width: 610, height: 460, fill: palette.panel, radius: 28, shadow: softShadow() }))
|
|
|
|
|
- elements.push(textElement({ left: 154, top: 244, width: 460, height: 44, content: studentHeadline(page) || "课后练习", fontSize: 34, color: palette.primary, bold: true }))
|
|
|
|
|
- elements.push(...compactBullets(activitySteps(page, 4), palette, 154, 340, 480, "做"))
|
|
|
|
|
- elements.push(...visualBlock(page, palette, { left: 850, top: 200, width: 500, height: 420 }))
|
|
|
|
|
|
|
+ const elements = slideHeader(page, outline, palette, "课后延伸")
|
|
|
|
|
+ elements.push(shapeElement({ left: 92, top: 210, width: 1416, height: 440, fill: palette.surface, radius: 8, shadow: softShadow() }))
|
|
|
|
|
+ activitySteps(page, 4).forEach((step, index) => {
|
|
|
|
|
+ const top = 270 + index * 86
|
|
|
|
|
+ elements.push(textElement({ left: 150, top, width: 70, height: 48, content: `${index + 1}.`, fontSize: 34, color: palette.accent, bold: true }))
|
|
|
|
|
+ elements.push(textElement({ left: 230, top, width: 1180, height: 58, content: step, fontSize: fitFont(step, 1180, 58, 31, 24), color: palette.text, bold: true }))
|
|
|
|
|
+ })
|
|
|
return elements
|
|
return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function largeStudentPanel(page: AiCoursewarePage, palette: Palette, left: number, top: number, width: number, height: number): PPTElement[] {
|
|
|
|
|
|
|
+function contentTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, index: number): PPTElement[] {
|
|
|
|
|
+ return knowledgeTemplate(page, outline, palette, index)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function slideHeader(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, eyebrow: string): PPTElement[] {
|
|
|
|
|
+ const title = displayPageTitle(page, outline)
|
|
|
|
|
+ return [
|
|
|
|
|
+ textElement({ left: 92, top: 64, width: 300, height: 34, content: eyebrow, fontSize: 20, color: palette.secondary, bold: true }),
|
|
|
|
|
+ textElement({ left: 92, top: 102, width: 1320, height: 66, content: title, fontSize: fitFont(title, 1320, 66, 42, 32), color: palette.text, bold: true }),
|
|
|
|
|
+ shapeElement({ left: 1450, top: 72, width: 58, height: 8, fill: palette.accent, radius: 4 }),
|
|
|
|
|
+ ]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function studentPanel(page: AiCoursewarePage, palette: Palette, left: number, top: number, width: number): PPTElement[] {
|
|
|
const elements: PPTElement[] = []
|
|
const elements: PPTElement[] = []
|
|
|
const headline = studentHeadline(page)
|
|
const headline = studentHeadline(page)
|
|
|
- elements.push(shapeElement({ left, top, width, height, fill: palette.panel, radius: 26, shadow: softShadow() }))
|
|
|
|
|
- if (headline) {
|
|
|
|
|
- elements.push(textElement({ left: left + 42, top: top + 40, width: width - 84, height: 44, content: headline, fontSize: 34, color: palette.primary, bold: true }))
|
|
|
|
|
- }
|
|
|
|
|
- studentPoints(page, 4).forEach((point, index) => {
|
|
|
|
|
- const itemTop = top + (headline ? 130 : 68) + index * 70
|
|
|
|
|
- elements.push(shapeElement({ left: left + 42, top: itemTop + 8, width: 26, height: 26, fill: index % 2 ? palette.secondary : palette.primary, radius: 7 }))
|
|
|
|
|
- elements.push(textElement({ left: left + 92, top: itemTop, width: width - 140, height: 38, content: point, fontSize: 27, color: palette.text, lineHeight: 1.25 }))
|
|
|
|
|
|
|
+ if (headline) elements.push(textElement({ left, top, width, height: 90, content: headline, fontSize: fitFont(headline, width, 90, 36, 27), color: palette.primary, bold: true }))
|
|
|
|
|
+ studentPoints(page, 3).forEach((point, index) => {
|
|
|
|
|
+ const itemTop = top + (headline ? 120 : 0) + index * 74
|
|
|
|
|
+ elements.push(shapeElement({ left, top: itemTop + 8, width: 18, height: 18, fill: index % 2 ? palette.secondary : palette.accent, radius: 4 }))
|
|
|
|
|
+ elements.push(textElement({ left: left + 38, top: itemTop, width: width - 38, height: 54, content: point, fontSize: fitFont(point, width - 38, 54, 27, 21), color: palette.text, lineHeight: 1.3 }))
|
|
|
})
|
|
})
|
|
|
return elements
|
|
return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function compactBullets(items: string[], palette: Palette, left: number, top: number, width: number, mark: string): PPTElement[] {
|
|
|
|
|
|
|
+function storySequence(items: string[], palette: Palette, left: number, top: number, width: number): PPTElement[] {
|
|
|
const elements: PPTElement[] = []
|
|
const elements: PPTElement[] = []
|
|
|
|
|
+ const count = Math.max(1, Math.min(items.length, 4))
|
|
|
|
|
+ const gap = 28
|
|
|
|
|
+ const cardWidth = (width - gap * (count - 1)) / count
|
|
|
items.slice(0, 4).forEach((item, index) => {
|
|
items.slice(0, 4).forEach((item, index) => {
|
|
|
- const itemTop = top + index * 64
|
|
|
|
|
- elements.push(shapeElement({ left, top: itemTop, width, height: 48, fill: "#FFFFFF", opacity: 0.94, radius: 14, shadow: lightShadow() }))
|
|
|
|
|
- elements.push(shapeElement({ left: left + 20, top: itemTop + 13, width: 22, height: 22, fill: index % 2 ? palette.secondary : palette.primary, radius: 5 }))
|
|
|
|
|
- elements.push(textElement({ left: left + 62, top: itemTop + 8, width: width - 88, height: 30, content: item, fontSize: 24, color: palette.text }))
|
|
|
|
|
|
|
+ const cardLeft = left + index * (cardWidth + gap)
|
|
|
|
|
+ elements.push(shapeElement({ left: cardLeft, top, width: cardWidth, height: 190, fill: index % 2 === 0 ? palette.soft : palette.surface, radius: 8, shadow: lightShadow() }))
|
|
|
|
|
+ elements.push(textElement({ left: cardLeft + 24, top: top + 24, width: 64, height: 38, content: `0${index + 1}`, fontSize: 26, color: index % 2 === 0 ? palette.primary : palette.secondary, bold: true }))
|
|
|
|
|
+ elements.push(textElement({ left: cardLeft + 24, top: top + 82, width: cardWidth - 48, height: 78, content: item, fontSize: fitFont(item, cardWidth - 48, 78, 27, 21), color: palette.text, bold: true, lineHeight: 1.3 }))
|
|
|
})
|
|
})
|
|
|
return elements
|
|
return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function visualBlock(page: AiCoursewarePage, palette: Palette, rect: VisualRect): PPTElement[] {
|
|
|
|
|
- if (page.image?.url) {
|
|
|
|
|
- return [
|
|
|
|
|
- shapeElement({ left: rect.left - 14, top: rect.top + 16, width: rect.width, height: rect.height, fill: palette.warm, opacity: 0.45, radius: 26 }),
|
|
|
|
|
- imageElement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height, src: page.image.url, radius: 28 })
|
|
|
|
|
- ]
|
|
|
|
|
- }
|
|
|
|
|
- return fallbackVisual(page, palette, rect)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function heroVisual(page: AiCoursewarePage, palette: Palette, rect: VisualRect): PPTElement[] {
|
|
|
|
|
- return visualBlock(page, palette, rect)
|
|
|
|
|
|
|
+function knowledgeList(items: string[], palette: Palette, left: number, top: number, width: number): PPTElement[] {
|
|
|
|
|
+ const elements: PPTElement[] = []
|
|
|
|
|
+ items.forEach((item, index) => {
|
|
|
|
|
+ const itemTop = top + index * 108
|
|
|
|
|
+ elements.push(shapeElement({ left, top: itemTop, width, height: 82, fill: index % 2 === 0 ? palette.surface : palette.soft, radius: 8, shadow: lightShadow() }))
|
|
|
|
|
+ elements.push(shapeElement({ left, top: itemTop, width: 14, height: 82, fill: index % 2 === 0 ? palette.primary : palette.secondary, radius: 0 }))
|
|
|
|
|
+ elements.push(textElement({ left: left + 42, top: itemTop + 18, width: width - 74, height: 48, content: item, fontSize: fitFont(item, width - 74, 48, 29, 22), color: palette.text, bold: true }))
|
|
|
|
|
+ })
|
|
|
|
|
+ return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function fallbackVisual(page: AiCoursewarePage, palette: Palette, rect: VisualRect): PPTElement[] {
|
|
|
|
|
|
|
+function melodyStaff(left: number, top: number, width: number, palette: Palette): PPTElement[] {
|
|
|
const elements: PPTElement[] = []
|
|
const elements: PPTElement[] = []
|
|
|
- const visual = visualConfig(page, palette)
|
|
|
|
|
- const label = visualMain(page) || visual.label
|
|
|
|
|
- const tags = visualElements(page).slice(0, 3)
|
|
|
|
|
- elements.push(shapeElement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height, fill: visual.light, radius: 28, shadow: softShadow() }))
|
|
|
|
|
- elements.push(shapeElement({ left: rect.left + rect.width * 0.18, top: rect.top + rect.height * 0.13, width: rect.width * 0.64, height: rect.height * 0.36, fill: "#FFFFFF", opacity: 0.92, radius: 30 }))
|
|
|
|
|
- elements.push(textElement({ left: rect.left + rect.width * 0.42, top: rect.top + rect.height * 0.2, width: rect.width * 0.18, height: 58, content: visual.icon, fontSize: 54, color: visual.color, bold: true }))
|
|
|
|
|
- elements.push(...soundWave(rect.left + rect.width * 0.24, rect.top + rect.height * 0.58, palette, rect.width * 0.5, 70))
|
|
|
|
|
- elements.push(textElement({ left: rect.left + rect.width * 0.14, top: rect.top + rect.height * 0.43, width: rect.width * 0.72, height: 34, content: trimText(label, 14), fontSize: 21, color: visual.color, bold: true }))
|
|
|
|
|
- tags.forEach((tag, index) => {
|
|
|
|
|
- const tagTop = rect.top + rect.height * 0.76 + index * 34
|
|
|
|
|
- elements.push(shapeElement({ left: rect.left + rect.width * 0.22, top: tagTop, width: rect.width * 0.56, height: 26, fill: "#FFFFFF", opacity: 0.86, radius: 13 }))
|
|
|
|
|
- elements.push(textElement({ left: rect.left + rect.width * 0.26, top: tagTop + 5, width: rect.width * 0.48, height: 16, content: trimText(tag, 12), fontSize: 14, color: palette.muted }))
|
|
|
|
|
|
|
+ for (let i = 0; i < 5; i++) elements.push(lineElement(left, top + i * 34, left + width, top + i * 34, "rgba(255,255,255,0.55)"))
|
|
|
|
|
+ ;[0.12, 0.36, 0.62, 0.84].forEach((position, index) => {
|
|
|
|
|
+ elements.push(shapeElement({ left: left + width * position, top: top + 48 + (index % 2) * 30, width: 34, height: 28, fill: index % 2 ? palette.accent : palette.warm, radius: 8 }))
|
|
|
|
|
+ elements.push(lineElement(left + width * position + 30, top + 10 + (index % 2) * 30, left + width * position + 30, top + 60 + (index % 2) * 30, index % 2 ? palette.accent : palette.warm, 5))
|
|
|
})
|
|
})
|
|
|
return elements
|
|
return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function addSlideTitle(elements: PPTElement[], page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette) {
|
|
|
|
|
- elements.push(titleMark(92, 74, palette.primary))
|
|
|
|
|
- elements.push(textElement({ left: 92, top: 102, width: 1120, height: 58, content: page.title || outline.title || outline.chapterName, fontSize: 40, color: palette.primary, bold: true }))
|
|
|
|
|
|
|
+function soundWave(left: number, top: number, palette: Palette, width: number, maxHeight: number): PPTElement[] {
|
|
|
|
|
+ const heights = [0.28, 0.55, 0.82, 0.45, 1, 0.7, 0.38, 0.64, 0.32]
|
|
|
|
|
+ return heights.map((scale, index) => shapeElement({
|
|
|
|
|
+ left: left + index * (width / heights.length), top: top + (maxHeight - maxHeight * scale) / 2,
|
|
|
|
|
+ width: Math.max(18, width / 24), height: maxHeight * scale,
|
|
|
|
|
+ fill: index % 3 === 0 ? palette.accent : index % 2 === 0 ? palette.secondary : palette.primary, radius: 8,
|
|
|
|
|
+ }))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function addTeacherAction(elements: PPTElement[], page: AiCoursewarePage, palette: Palette, left: number, top: number, width: number, fallback = "") {
|
|
|
|
|
- const text = fallback || page.interaction?.question || page.classroomAction?.studentTask || activityTask(page)
|
|
|
|
|
- if (!text) return
|
|
|
|
|
- elements.push(shapeElement({ left, top, width, height: 54, fill: palette.panelAlt, opacity: 0.92, radius: 14 }))
|
|
|
|
|
- elements.push(textElement({ left: left + 24, top: top + 15, width: width - 48, height: 24, content: trimText(text, 48), fontSize: 20, color: palette.text }))
|
|
|
|
|
|
|
+function addClassroomCue(elements: PPTElement[], page: AiCoursewarePage, palette: Palette) {
|
|
|
|
|
+ const cue = page.interaction?.question || activityTask(page)
|
|
|
|
|
+ if (!cue) return
|
|
|
|
|
+ elements.push(shapeElement({ left: 92, top: 754, width: 1416, height: 62, fill: palette.soft, radius: 8 }))
|
|
|
|
|
+ elements.push(textElement({ left: 120, top: 772, width: 1360, height: 30, content: cue, fontSize: fitFont(cue, 1360, 30, 22, 18), color: palette.text, bold: true }))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function titleMark(left: number, top: number, color: string): PPTShapeElement {
|
|
|
|
|
- return shapeElement({ left, top, width: 78, height: 8, fill: color, radius: 5 })
|
|
|
|
|
|
|
+function normalizeOutline(outline: AiCoursewareOutline): AiCoursewareOutline {
|
|
|
|
|
+ const chapter = cleanText(outline.chapterName || outline.title || "音乐课件")
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...outline,
|
|
|
|
|
+ title: GENERIC_PAGE_TITLES.has(cleanText(outline.title).toLowerCase()) ? chapter : cleanText(outline.title || chapter),
|
|
|
|
|
+ pages: (outline.pages || []).map((page, index) => ({
|
|
|
|
|
+ ...page,
|
|
|
|
|
+ pageNo: index + 1,
|
|
|
|
|
+ title: index === 0 && GENERIC_PAGE_TITLES.has(cleanText(page.title).toLowerCase()) ? chapter : cleanText(page.title || chapter),
|
|
|
|
|
+ bullets: (page.bullets || []).map(cleanText).filter(Boolean),
|
|
|
|
|
+ imageKeywords: (page.imageKeywords || []).map(cleanText).filter(Boolean),
|
|
|
|
|
+ })),
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function soundWave(left: number, top: number, palette: Palette, width = 330, maxHeight = 120): PPTElement[] {
|
|
|
|
|
- const elements: PPTElement[] = []
|
|
|
|
|
- const heights = [42, 76, 108, 70, 120, 88, 52].map(height => (height / 120) * maxHeight)
|
|
|
|
|
- heights.forEach((height, index) => {
|
|
|
|
|
- const barWidth = Math.max(14, width / 22)
|
|
|
|
|
- const gap = Math.max(18, width / 9)
|
|
|
|
|
- elements.push(shapeElement({ left: left + index * gap, top: top + (maxHeight - height) / 2, width: barWidth, height, fill: index % 2 === 0 ? palette.primary : palette.accent, radius: 8 }))
|
|
|
|
|
- })
|
|
|
|
|
- return elements
|
|
|
|
|
|
|
+function pageRole(page: AiCoursewarePage, index: number): PageRole {
|
|
|
|
|
+ if (index === 0) return "cover"
|
|
|
|
|
+ const kind = `${page.type || ""} ${page.visualPlan?.layout || ""} ${page.visual?.layout || ""}`.toLowerCase()
|
|
|
|
|
+ if (kind.includes("goal")) return "goal"
|
|
|
|
|
+ if (kind.includes("listen")) return "listen"
|
|
|
|
|
+ if (kind.includes("lyric") || kind.includes("sing")) return "lyric"
|
|
|
|
|
+ if (kind.includes("rhythm") || kind.includes("skill")) return "rhythm"
|
|
|
|
|
+ if (kind.includes("knowledge") || kind.includes("extension")) return "knowledge"
|
|
|
|
|
+ if (kind.includes("interaction")) return "interaction"
|
|
|
|
|
+ if (kind.includes("activity")) return "activity"
|
|
|
|
|
+ if (kind.includes("summary")) return "summary"
|
|
|
|
|
+ if (kind.includes("homework")) return "homework"
|
|
|
|
|
+ if (kind.includes("intro")) return "intro"
|
|
|
|
|
+ if (kind.includes("background") || kind.includes("story")) return "story"
|
|
|
|
|
+ return "content"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function baseElements(palette: Palette, options: AiPptOptions): PPTElement[] {
|
|
|
|
|
- const elements: PPTElement[] = []
|
|
|
|
|
- if (options.backgroundImage) {
|
|
|
|
|
- elements.push(shapeElement({ left: 0, top: 0, width: SLIDE_WIDTH, height: SLIDE_HEIGHT, fill: "rgba(255,255,255,0.82)", opacity: 0.96 }))
|
|
|
|
|
- }
|
|
|
|
|
- elements.push(shapeElement({ left: 0, top: 0, width: 10, height: SLIDE_HEIGHT, fill: palette.primary, radius: 0 }))
|
|
|
|
|
- elements.push(shapeElement({ left: 1260, top: 70, width: 210, height: 210, fill: palette.panelAlt, opacity: 0.28, radius: 100 }))
|
|
|
|
|
- elements.push(shapeElement({ left: 1380, top: 690, width: 88, height: 88, fill: palette.warm, opacity: 0.3, radius: 42 }))
|
|
|
|
|
- return elements
|
|
|
|
|
|
|
+function displayPageTitle(page: AiCoursewarePage, outline: AiCoursewareOutline) {
|
|
|
|
|
+ const title = cleanText(page.title)
|
|
|
|
|
+ return !title || GENERIC_PAGE_TITLES.has(title.toLowerCase()) ? chapterTitle(outline, page) : title
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function chapterTitle(outline: AiCoursewareOutline, page: AiCoursewarePage) {
|
|
|
|
|
+ return cleanText(outline.chapterName || outline.title || page.title || "音乐课件")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function conciseTextbook(value?: string) {
|
|
|
|
|
+ return cleanText(value).replace(/^.*?·/, "")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function hasImage(page: AiCoursewarePage) {
|
|
|
|
|
+ return Boolean(page.image?.url)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function studentHeadline(page: AiCoursewarePage) {
|
|
function studentHeadline(page: AiCoursewarePage) {
|
|
|
return cleanText(page.studentContent?.headline || page.subtitle || "")
|
|
return cleanText(page.studentContent?.headline || page.subtitle || "")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function studentPoints(page: AiCoursewarePage, limit: number): string[] {
|
|
|
|
|
|
|
+function studentPoints(page: AiCoursewarePage, limit: number) {
|
|
|
const structured = (page.studentContent?.points || []).map(cleanText).filter(Boolean)
|
|
const structured = (page.studentContent?.points || []).map(cleanText).filter(Boolean)
|
|
|
const legacy = (page.bullets || []).map(cleanText).filter(Boolean)
|
|
const legacy = (page.bullets || []).map(cleanText).filter(Boolean)
|
|
|
- const source = structured.length ? structured : legacy
|
|
|
|
|
- return (source.length ? source : [studentHeadline(page) || page.title || "课堂内容"]).slice(0, limit).map(item => trimText(item, 34))
|
|
|
|
|
|
|
+ const points = structured.length ? structured : legacy
|
|
|
|
|
+ return (points.length ? points : [studentHeadline(page) || cleanText(page.title) || "课堂内容"]).slice(0, limit)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function activityTask(page: AiCoursewarePage) {
|
|
function activityTask(page: AiCoursewarePage) {
|
|
|
return cleanText(page.classroomAction?.studentTask || page.activity?.task || "")
|
|
return cleanText(page.classroomAction?.studentTask || page.activity?.task || "")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function activitySteps(page: AiCoursewarePage, limit: number): string[] {
|
|
|
|
|
- const steps = [page.classroomAction?.studentTask, ...(page.activity?.steps || [])].map(cleanText).filter(Boolean)
|
|
|
|
|
- return (steps.length ? steps : studentPoints(page, limit)).slice(0, limit).map(item => trimText(item, 32))
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function pageTitle(page: AiCoursewarePage, outline: AiCoursewareOutline) {
|
|
|
|
|
- return cleanText(page.title || outline.title || outline.chapterName || "音乐课件")
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function pageKind(page: AiCoursewarePage) {
|
|
|
|
|
- return `${page.type || ""} ${page.visualPlan?.layout || ""} ${page.visual?.layout || ""}`.toLowerCase()
|
|
|
|
|
|
|
+function activitySteps(page: AiCoursewarePage, limit: number) {
|
|
|
|
|
+ const steps = (page.activity?.steps || []).map(cleanText).filter(Boolean)
|
|
|
|
|
+ const task = activityTask(page)
|
|
|
|
|
+ const values = steps.length ? steps : task ? [task, ...studentPoints(page, limit)] : studentPoints(page, limit)
|
|
|
|
|
+ return values.filter((value, index, all) => all.indexOf(value) === index).slice(0, limit)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function slideRemark(page: AiCoursewarePage) {
|
|
function slideRemark(page: AiCoursewarePage) {
|
|
|
return [page.speakerNotes, page.teacherTips, page.classroomAction?.teacherInstruction, page.teachingPurpose]
|
|
return [page.speakerNotes, page.teacherTips, page.classroomAction?.teacherInstruction, page.teachingPurpose]
|
|
|
- .map(cleanText)
|
|
|
|
|
- .filter(Boolean)
|
|
|
|
|
- .filter((item, index, array) => array.indexOf(item) === index)
|
|
|
|
|
- .join("\n")
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function visualMain(page: AiCoursewarePage) {
|
|
|
|
|
- return cleanText(page.visualPlan?.mainVisual || page.visual?.mainVisual || "")
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function visualElements(page: AiCoursewarePage) {
|
|
|
|
|
- const elements = (page.visualPlan?.elements || []).map(cleanText).filter(Boolean)
|
|
|
|
|
- const keywords = (page.visualPlan?.imageKeywords || page.visual?.imageKeywords || page.imageKeywords || []).map(cleanText).filter(Boolean)
|
|
|
|
|
- return elements.length ? elements : keywords
|
|
|
|
|
|
|
+ .map(cleanText).filter(Boolean).filter((item, index, all) => all.indexOf(item) === index).join("\n")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function visualConfig(page: AiCoursewarePage, palette: Palette) {
|
|
|
|
|
- const kind = pageKind(page)
|
|
|
|
|
- const label = trimText(visualMain(page) || page.visual?.style || "", 12)
|
|
|
|
|
- if (kind.includes("interaction") || kind.includes("activity")) return { icon: "?", label: label || "课堂互动", color: palette.accent, light: "#EEF2FF" }
|
|
|
|
|
- if (kind.includes("summary")) return { icon: "✓", label: label || "课堂小结", color: "#2E7D32", light: "#EAF7EC" }
|
|
|
|
|
- if (kind.includes("goal")) return { icon: "◎", label: label || "学习目标", color: "#7B1FA2", light: "#F3E8FF" }
|
|
|
|
|
- if (kind.includes("listen")) return { icon: "♪", label: label || "听赏任务", color: palette.secondary, light: "#E5F6F3" }
|
|
|
|
|
- if (kind.includes("rhythm") || kind.includes("skill")) return { icon: "♩", label: label || "节奏练习", color: palette.primary, light: "#FFF1E7" }
|
|
|
|
|
- if (kind.includes("homework")) return { icon: "✎", label: label || "课后练习", color: "#6D4C41", light: "#F3ECE8" }
|
|
|
|
|
- return { icon: "♫", label: label || "音乐课堂", color: palette.primary, light: palette.warm }
|
|
|
|
|
|
|
+function buildPalette(outline: AiCoursewareOutline): Palette {
|
|
|
|
|
+ const clean = outline.style === "clean" || /[初高]中|七年级|八年级|九年级/.test(outline.gradeName || "")
|
|
|
|
|
+ if (clean) return {
|
|
|
|
|
+ primary: "#2563EB", secondary: "#0F766E", accent: "#E9553D", warm: "#F5D06F",
|
|
|
|
|
+ background: "#F4F7FB", surface: "#FFFFFF", soft: "#EAF1F8", text: "#17222E", muted: "#64748B", dark: "#132A3A",
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ primary: "#E64A78", secondary: "#168F86", accent: "#F28C28", warm: "#FFD66B",
|
|
|
|
|
+ background: "#FFF9F5", surface: "#FFFFFF", soft: "#EAF7F4", text: "#263238", muted: "#6B7C8F", dark: "#283B4A",
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function backgroundConfig(palette: Palette, options: AiPptOptions): SlideBackground {
|
|
|
|
|
|
|
+function backgroundConfig(_role: PageRole, palette: Palette, options: AiPptOptions): SlideBackground {
|
|
|
if (options.backgroundImage) return { type: "image", image: options.backgroundImage, imageSize: "cover" }
|
|
if (options.backgroundImage) return { type: "image", image: options.backgroundImage, imageSize: "cover" }
|
|
|
return { type: "solid", color: palette.background }
|
|
return { type: "solid", color: palette.background }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function buildPalette(outline: AiCoursewareOutline): Palette {
|
|
|
|
|
- if (outline.style === "clean") {
|
|
|
|
|
- return {
|
|
|
|
|
- primary: "#2563EB",
|
|
|
|
|
- secondary: "#0F766E",
|
|
|
|
|
- accent: "#7C3AED",
|
|
|
|
|
- warm: "#FEF3C7",
|
|
|
|
|
- background: "#F7FAFC",
|
|
|
|
|
- panel: "#FFFFFF",
|
|
|
|
|
- panelAlt: "#EAF2FF",
|
|
|
|
|
- text: "#1F2933",
|
|
|
|
|
- muted: "#64748B"
|
|
|
|
|
- }
|
|
|
|
|
|
|
+function fitFont(content: string, width: number, height: number, preferred: number, minimum: number) {
|
|
|
|
|
+ const text = cleanText(content)
|
|
|
|
|
+ if (!text) return preferred
|
|
|
|
|
+ let size = preferred
|
|
|
|
|
+ while (size > minimum) {
|
|
|
|
|
+ const charsPerLine = Math.max(1, Math.floor(width / size))
|
|
|
|
|
+ const lines = Math.ceil(text.length / charsPerLine)
|
|
|
|
|
+ if (lines * size * 1.35 <= height) break
|
|
|
|
|
+ size -= 2
|
|
|
}
|
|
}
|
|
|
- const variants: Palette[] = [
|
|
|
|
|
- {
|
|
|
|
|
- primary: "#FF7A3D",
|
|
|
|
|
- secondary: "#2BAE9F",
|
|
|
|
|
- accent: "#5B8DEF",
|
|
|
|
|
- warm: "#FFE8B8",
|
|
|
|
|
- background: "#FFF7EC",
|
|
|
|
|
- panel: "#FFFFFF",
|
|
|
|
|
- panelAlt: "#EAF6F3",
|
|
|
|
|
- text: "#263238",
|
|
|
|
|
- muted: "#6B7C8F"
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- primary: "#EF5DA8",
|
|
|
|
|
- secondary: "#36B37E",
|
|
|
|
|
- accent: "#4C6FFF",
|
|
|
|
|
- warm: "#FFF0C7",
|
|
|
|
|
- background: "#FFF8F8",
|
|
|
|
|
- panel: "#FFFFFF",
|
|
|
|
|
- panelAlt: "#ECFDF5",
|
|
|
|
|
- text: "#263238",
|
|
|
|
|
- muted: "#6B7C8F"
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
- return variants[Math.abs((outline.chapterName || outline.title || "").length) % variants.length]
|
|
|
|
|
|
|
+ return size
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function textElement(params: {
|
|
|
|
|
- left: number
|
|
|
|
|
- top: number
|
|
|
|
|
- width: number
|
|
|
|
|
- height: number
|
|
|
|
|
- content: string
|
|
|
|
|
- fontSize: number
|
|
|
|
|
- color: string
|
|
|
|
|
- bold?: boolean
|
|
|
|
|
- lineHeight?: number
|
|
|
|
|
-}): PPTTextElement {
|
|
|
|
|
|
|
+function textElement(params: { left: number; top: number; width: number; height: number; content: string; fontSize: number; color: string; bold?: boolean; lineHeight?: number }): PPTTextElement {
|
|
|
return {
|
|
return {
|
|
|
- id: nanoid(10),
|
|
|
|
|
- type: "text",
|
|
|
|
|
- left: params.left,
|
|
|
|
|
- top: params.top,
|
|
|
|
|
- width: params.width,
|
|
|
|
|
- height: params.height,
|
|
|
|
|
- rotate: 0,
|
|
|
|
|
|
|
+ id: nanoid(10), type: "text", left: params.left, top: params.top, width: params.width, height: params.height, rotate: 0,
|
|
|
content: `<div style="font-family:${FONT_NAME};font-size:${params.fontSize}px;${params.bold ? "font-weight:700;" : ""}">${escapeHtml(params.content)}</div>`,
|
|
content: `<div style="font-family:${FONT_NAME};font-size:${params.fontSize}px;${params.bold ? "font-weight:700;" : ""}">${escapeHtml(params.content)}</div>`,
|
|
|
- defaultFontName: FONT_NAME,
|
|
|
|
|
- defaultColor: params.color,
|
|
|
|
|
- lineHeight: params.lineHeight || 1.25,
|
|
|
|
|
- wordSpace: 0,
|
|
|
|
|
- opacity: 1
|
|
|
|
|
|
|
+ defaultFontName: FONT_NAME, defaultColor: params.color, lineHeight: params.lineHeight || 1.25, wordSpace: 0, opacity: 1,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function imageElement(params: { left: number; top: number; width: number; height: number; src: string; radius?: number }): PPTImageElement {
|
|
|
|
|
|
|
+function imageElement(page: AiCoursewarePage, rect: ImageRect): PPTImageElement {
|
|
|
|
|
+ const width = page.image?.width || 0
|
|
|
|
|
+ const height = page.image?.height || 0
|
|
|
return {
|
|
return {
|
|
|
- id: nanoid(10),
|
|
|
|
|
- type: "image",
|
|
|
|
|
- left: params.left,
|
|
|
|
|
- top: params.top,
|
|
|
|
|
- width: params.width,
|
|
|
|
|
- height: params.height,
|
|
|
|
|
- rotate: 0,
|
|
|
|
|
- src: params.src,
|
|
|
|
|
- fixedRatio: false,
|
|
|
|
|
- radius: params.radius || 18,
|
|
|
|
|
- shadow: softShadow()
|
|
|
|
|
|
|
+ id: nanoid(10), type: "image", left: rect.left, top: rect.top, width: rect.width, height: rect.height, rotate: 0,
|
|
|
|
|
+ src: page.image?.url || "", fixedRatio: false, radius: rect.radius || 0, shadow: rect.radius ? softShadow() : undefined,
|
|
|
|
|
+ clip: width > 0 && height > 0 ? { shape: "rect", range: coverRange(width, height, rect.width, rect.height) } : undefined,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function shapeElement(params: {
|
|
|
|
|
- left: number
|
|
|
|
|
- top: number
|
|
|
|
|
- width: number
|
|
|
|
|
- height: number
|
|
|
|
|
- fill: string
|
|
|
|
|
- opacity?: number
|
|
|
|
|
- radius?: number
|
|
|
|
|
- shadow?: PPTElementShadow
|
|
|
|
|
-}): PPTShapeElement {
|
|
|
|
|
- return {
|
|
|
|
|
- id: nanoid(10),
|
|
|
|
|
- type: "shape",
|
|
|
|
|
- left: params.left,
|
|
|
|
|
- top: params.top,
|
|
|
|
|
- width: params.width,
|
|
|
|
|
- height: params.height,
|
|
|
|
|
- rotate: 0,
|
|
|
|
|
- viewBox: [200, 200],
|
|
|
|
|
- path: roundedRectPath(params.radius ?? 18),
|
|
|
|
|
- fixedRatio: false,
|
|
|
|
|
- fill: params.fill,
|
|
|
|
|
- opacity: params.opacity ?? 1,
|
|
|
|
|
- shadow: params.shadow
|
|
|
|
|
|
|
+function coverRange(sourceWidth: number, sourceHeight: number, targetWidth: number, targetHeight: number): [[number, number], [number, number]] {
|
|
|
|
|
+ const sourceRatio = sourceWidth / sourceHeight
|
|
|
|
|
+ const targetRatio = targetWidth / targetHeight
|
|
|
|
|
+ if (sourceRatio > targetRatio) {
|
|
|
|
|
+ const visible = (targetRatio / sourceRatio) * 100
|
|
|
|
|
+ const start = (100 - visible) / 2
|
|
|
|
|
+ return [[start, 0], [100 - start, 100]]
|
|
|
}
|
|
}
|
|
|
|
|
+ const visible = (sourceRatio / targetRatio) * 100
|
|
|
|
|
+ const start = (100 - visible) / 2
|
|
|
|
|
+ return [[0, start], [100, 100 - start]]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function keepInCanvas(elements: PPTElement[]) {
|
|
|
|
|
- return elements.map(element => ({
|
|
|
|
|
- ...element,
|
|
|
|
|
- left: Math.max(0, Math.min(element.left, SLIDE_WIDTH - 1)),
|
|
|
|
|
- top: Math.max(0, Math.min(element.top, SLIDE_HEIGHT - 1)),
|
|
|
|
|
- width: Math.max(1, Math.min(element.width, SLIDE_WIDTH - element.left)),
|
|
|
|
|
- height: Math.max(1, Math.min(element.height, SLIDE_HEIGHT - element.top))
|
|
|
|
|
- })) as PPTElement[]
|
|
|
|
|
|
|
+function shapeElement(params: { left: number; top: number; width: number; height: number; fill: string; opacity?: number; radius?: number; shadow?: PPTElementShadow; gradient?: Gradient }): PPTShapeElement {
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: nanoid(10), type: "shape", left: params.left, top: params.top, width: params.width, height: params.height, rotate: 0,
|
|
|
|
|
+ viewBox: [200, 200], path: roundedRectPath(params.radius || 0), fixedRatio: false, fill: params.fill,
|
|
|
|
|
+ opacity: params.opacity ?? 1, shadow: params.shadow, gradient: params.gradient,
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function softShadow(): PPTElementShadow {
|
|
|
|
|
- return { h: 0, v: 8, blur: 22, color: "rgba(38, 50, 56, 0.14)" }
|
|
|
|
|
|
|
+function lineElement(x1: number, y1: number, x2: number, y2: number, color: string, width = 2): PPTLineElement {
|
|
|
|
|
+ const left = Math.min(x1, x2)
|
|
|
|
|
+ const top = Math.min(y1, y2)
|
|
|
|
|
+ const lineWidth = Math.abs(x2 - x1)
|
|
|
|
|
+ const lineHeight = Math.abs(y2 - y1)
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: nanoid(10), type: "line", left, top, width,
|
|
|
|
|
+ start: [x1 === left ? 0 : lineWidth, y1 === top ? 0 : lineHeight],
|
|
|
|
|
+ end: [x2 === left ? 0 : lineWidth, y2 === top ? 0 : lineHeight],
|
|
|
|
|
+ style: "solid", color, points: ["", ""],
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function lightShadow(): PPTElementShadow {
|
|
|
|
|
- return { h: 0, v: 6, blur: 16, color: "rgba(38, 50, 56, 0.1)" }
|
|
|
|
|
|
|
+function keepInCanvas(elements: PPTElement[]) {
|
|
|
|
|
+ return elements.map(element => {
|
|
|
|
|
+ const left = Math.max(0, Math.min(element.left, SLIDE_WIDTH - 1))
|
|
|
|
|
+ const top = Math.max(0, Math.min(element.top, SLIDE_HEIGHT - 1))
|
|
|
|
|
+ if (element.type === "line") return { ...element, left, top, width: Math.max(1, Math.min(element.width, 20)) }
|
|
|
|
|
+ return { ...element, left, top, width: Math.max(1, Math.min(element.width, SLIDE_WIDTH - left)), height: Math.max(1, Math.min(element.height, SLIDE_HEIGHT - top)) }
|
|
|
|
|
+ }) as PPTElement[]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function runPreflight(slides: Slide[]) {
|
|
|
|
|
+ slides.forEach((slide, index) => {
|
|
|
|
|
+ const invalid = slide.elements.filter(element => element.left < 0 || element.top < 0 || element.left + element.width > SLIDE_WIDTH + 1 || (element.type !== "line" && element.top + element.height > SLIDE_HEIGHT + 1))
|
|
|
|
|
+ const emptyText = slide.elements.filter(element => element.type === "text" && !stripHtml(element.content))
|
|
|
|
|
+ if (invalid.length || emptyText.length) console.warn("AI PPT preflight issue", { pageNo: index + 1, invalid: invalid.length, emptyText: emptyText.length })
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function roundedRectPath(radius: number) {
|
|
function roundedRectPath(radius: number) {
|
|
@@ -497,15 +513,22 @@ function roundedRectPath(radius: number) {
|
|
|
return `M ${r} 0 L ${200 - r} 0 Q 200 0 200 ${r} L 200 ${200 - r} Q 200 200 ${200 - r} 200 L ${r} 200 Q 0 200 0 ${200 - r} L 0 ${r} Q 0 0 ${r} 0 Z`
|
|
return `M ${r} 0 L ${200 - r} 0 Q 200 0 200 ${r} L 200 ${200 - r} Q 200 200 ${200 - r} 200 L ${r} 200 Q 0 200 0 ${200 - r} L 0 ${r} Q 0 0 ${r} 0 Z`
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function cleanText(value?: string) {
|
|
|
|
|
- return String(value || "").replace(/\s+/g, " ").trim()
|
|
|
|
|
|
|
+function softShadow(): PPTElementShadow {
|
|
|
|
|
+ return { h: 0, v: 8, blur: 22, color: "rgba(23, 34, 46, 0.14)" }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function trimText(value: string, maxLength: number) {
|
|
|
|
|
- const text = cleanText(value)
|
|
|
|
|
- return text.length > maxLength ? `${text.slice(0, maxLength - 1)}...` : text
|
|
|
|
|
|
|
+function lightShadow(): PPTElementShadow {
|
|
|
|
|
+ return { h: 0, v: 4, blur: 14, color: "rgba(23, 34, 46, 0.1)" }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function cleanText(value?: string) {
|
|
|
|
|
+ return String(value || "").replace(/\s+/g, " ").trim()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function escapeHtml(value: string) {
|
|
function escapeHtml(value: string) {
|
|
|
return cleanText(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
return cleanText(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+function stripHtml(value: string) {
|
|
|
|
|
+ return value.replace(/<[^>]+>/g, "").trim()
|
|
|
|
|
+}
|