feat: 新增 AI 驱动句子云海报生成器 - #1
Conversation
Co-authored-by: traeagent <traeagent@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 44c12622f9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| page-break-inside: avoid; | ||
| } | ||
| .paper-canvas::before { display: none !important; } | ||
| .paper-content { transform: var(--print-transform, none) !important; } |
There was a problem hiding this comment.
Don't override the computed print scale
When the user clicks Print Poster, applyPrintScale() writes a scaled inline transform, but this @media print rule is !important and forces .paper-content back to --print-transform, which is only the unscaled translate set by computeContentTransform(). On A4 portrait, for example, the layout is 1131×1600 design pixels while the printed page is about 794px wide, so the poster content is clipped instead of being scaled to the physical page.
Useful? React with 👍 / 👎.
| window.addEventListener('resize', () => { | ||
| fitPaperToStage(); | ||
| }); |
There was a problem hiding this comment.
Recompute the content transform after resizing
When the viewport changes size after the poster has rendered, this handler only resizes the paper element; the existing .paper-content transform still contains the old cssScale from the previous computeContentTransform() call. Narrowing the window can therefore leave quotes too large and clipped by the paper, while widening leaves the poster unnecessarily small until another control triggers a full re-render.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Code Review
This pull request introduces an AI-driven Interactive Quote Cloud & Poster Generator as a single-file HTML application, complete with specifications, checklists, and task documentation. The application features OpenRouter API integration for quote weight analysis, an Archimedean spiral layout algorithm with AABB collision detection, responsive canvas scaling, and print pipeline support. The review feedback highlights several key improvements: resolving a print scaling issue where the scale factor was overridden by CSS by dynamically updating the custom property, enhancing layout accuracy by passing weight-specific font weights and styles to the text measurement function instead of using a hardcoded value, and ensuring proper content scaling on window resize by invoking the transform computation in the resize handler.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| $content.style.transformOrigin = '0 0'; | ||
| $content.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`; | ||
| } |
There was a problem hiding this comment.
问题分析:在 @media print 样式中,.paper-content 的 transform 被设置为了 var(--print-transform, none) !important。由于 !important 的存在,JS 中通过 $content.style.transform 设置的缩放(scale(${scale}))在打印时会被完全覆盖。而 --print-transform 变量仅在 computeContentTransform 中被设置为不带缩放的 translate,这导致打印预览和实际打印时海报内容无法正确缩放以适应纸张大小,从而产生裁剪或溢出。\n\n解决方案:在 applyPrintScale 中,同步更新 $paper 上的 --print-transform 自定义属性,使其包含计算后的 scale,从而确保打印管线能够正确应用缩放。
| $content.style.transformOrigin = '0 0'; | |
| $content.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`; | |
| } | |
| $content.style.transformOrigin = '0 0'; | |
| $content.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`; | |
| $paper.style.setProperty('--print-transform', `translate(${tx}px, ${ty}px) scale(${scale})`); | |
| } |
| function setMeasureFont(fontSize, fontWeight) { | ||
| _mctx.font = `${fontWeight} ${fontSize}px ${FONT_STACK}`; | ||
| } | ||
|
|
||
| /** | ||
| * Word-wrap a string into lines that fit within maxWidth, measured with | ||
| * context.measureText(). Supports CJK (per-character fallback) and Latin | ||
| * (whitespace tokenization). Returns {lines, width, height}. | ||
| */ | ||
| function measureMultilineText(text, fontSize, fontWeight, maxWidth) { | ||
| setMeasureFont(fontSize, fontWeight); |
There was a problem hiding this comment.
问题分析:在 spiralLayout 中调用 measureMultilineText 时,字体粗细(font weight)被硬编码为 700,且未考虑字体样式(font style)。然而,CSS 中不同权重的 quote 实际渲染样式不同:w3 为 700,w2 为 500,w1 为 400 且为斜体(italic)。这种测量与渲染样式的不一致会导致 Canvas 文本测量不准确,从而可能引起布局重叠或不必要的空白。\n\n解决方案:建议在 paramsForWeight 中返回对应的 fontWeight 和 fontStyle,并在测量时动态传入,以确保测量与渲染的字体指标完全一致。
| function setMeasureFont(fontSize, fontWeight) { | |
| _mctx.font = `${fontWeight} ${fontSize}px ${FONT_STACK}`; | |
| } | |
| /** | |
| * Word-wrap a string into lines that fit within maxWidth, measured with | |
| * context.measureText(). Supports CJK (per-character fallback) and Latin | |
| * (whitespace tokenization). Returns {lines, width, height}. | |
| */ | |
| function measureMultilineText(text, fontSize, fontWeight, maxWidth) { | |
| setMeasureFont(fontSize, fontWeight); | |
| function setMeasureFont(fontSize, fontWeight, fontStyle = 'normal') { | |
| _mctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${FONT_STACK}`; | |
| } | |
| /** | |
| * Word-wrap a string into lines that fit within maxWidth, measured with | |
| * context.measureText(). Supports CJK (per-character fallback) and Latin | |
| * (whitespace tokenization). Returns {lines, width, height}. | |
| */ | |
| function measureMultilineText(text, fontSize, fontWeight, maxWidth, fontStyle = 'normal') { | |
| setMeasureFont(fontSize, fontWeight, fontStyle); |
| function paramsForWeight(weight) { | ||
| // Returns per-weight type/layout params | ||
| switch (weight) { | ||
| case 3: return { fontSize: 64, lineHeight: 64*1.18, maxWidth: 760, color: 'var(--w3-color)', className: 'w3' }; | ||
| case 2: return { fontSize: 40, lineHeight: 40*1.18, maxWidth: 560, color: 'var(--w2-color)', className: 'w2' }; | ||
| default:return { fontSize: 24, lineHeight: 24*1.18, maxWidth: 360, color: 'var(--w1-color)', className: 'w1' }; | ||
| } | ||
| } |
There was a problem hiding this comment.
问题分析:为了配合 Canvas 文本测量的精确度提升,我们需要在 paramsForWeight 中定义每个权重对应的 fontWeight 和 fontStyle,以便在测量和渲染时使用完全一致的字体属性。
| function paramsForWeight(weight) { | |
| // Returns per-weight type/layout params | |
| switch (weight) { | |
| case 3: return { fontSize: 64, lineHeight: 64*1.18, maxWidth: 760, color: 'var(--w3-color)', className: 'w3' }; | |
| case 2: return { fontSize: 40, lineHeight: 40*1.18, maxWidth: 560, color: 'var(--w2-color)', className: 'w2' }; | |
| default:return { fontSize: 24, lineHeight: 24*1.18, maxWidth: 360, color: 'var(--w1-color)', className: 'w1' }; | |
| } | |
| } | |
| function paramsForWeight(weight) { | |
| // Returns per-weight type/layout params | |
| switch (weight) { | |
| case 3: return { fontSize: 64, lineHeight: 64*1.18, maxWidth: 760, color: 'var(--w3-color)', className: 'w3', fontWeight: 700, fontStyle: 'normal' }; | |
| case 2: return { fontSize: 40, lineHeight: 40*1.18, maxWidth: 560, color: 'var(--w2-color)', className: 'w2', fontWeight: 500, fontStyle: 'normal' }; | |
| default:return { fontSize: 24, lineHeight: 24*1.18, maxWidth: 360, color: 'var(--w1-color)', className: 'w1', fontWeight: 400, fontStyle: 'italic' }; | |
| } | |
| } |
| for (let qi = 0; qi < sorted.length; qi++) { | ||
| const q = sorted[qi]; | ||
| const p = paramsForWeight(q.weight); | ||
| const m = measureMultilineText(q.text, p.fontSize, 700, p.maxWidth); |
There was a problem hiding this comment.
| window.addEventListener('resize', () => { | ||
| fitPaperToStage(); | ||
| }); |
There was a problem hiding this comment.
问题分析:当浏览器窗口大小改变时,resize 事件监听器仅调用了 fitPaperToStage() 来调整纸张容器的大小,但没有重新计算并应用内容的缩放和居中偏移(即未调用 computeContentTransform())。这会导致在调整窗口大小后,句子云内容无法自适应缩放,出现内容过大溢出或过小留白的问题。\n\n解决方案:在 resize 事件监听器中,在调用 fitPaperToStage() 后紧接着调用 computeContentTransform(),以确保内容缩放实时更新。
| window.addEventListener('resize', () => { | |
| fitPaperToStage(); | |
| }); | |
| window.addEventListener('resize', () => { | |
| fitPaperToStage(); | |
| computeContentTransform(); | |
| }); |
There was a problem hiding this comment.
Pull request overview
This PR adds a zero-dependency single-file web app (index.html) that generates an AI-weighted quote-cloud poster with Archimedean-spiral layout, interactive focus-on-hover, paper sizing/orientation controls, and a print pipeline. It also adds accompanying spec/task/checklist documentation for the feature.
Changes:
- Added a standalone HTML/CSS/JS poster generator implementing canvas-based text measurement, spiral placement with AABB collision detection, and interactive rendering.
- Integrated OpenRouter (
google/gemini-flash-1.5) weight analysis with a deterministic local heuristic fallback. - Added feature specifications, task breakdown, and acceptance checklist under
.trae/specs/....
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
index.html |
New single-file poster generator app (layout, AI weighting, controls, print, interactions). |
.trae/specs/create-quote-cloud-poster-generator/spec.md |
New spec describing functional/UX requirements and scenarios. |
.trae/specs/create-quote-cloud-poster-generator/tasks.md |
New implementation task plan and dependencies. |
.trae/specs/create-quote-cloud-poster-generator/checklist.md |
New acceptance checklist for feature verification. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function paramsForWeight(weight) { | ||
| // Returns per-weight type/layout params | ||
| switch (weight) { | ||
| case 3: return { fontSize: 64, lineHeight: 64*1.18, maxWidth: 760, color: 'var(--w3-color)', className: 'w3' }; | ||
| case 2: return { fontSize: 40, lineHeight: 40*1.18, maxWidth: 560, color: 'var(--w2-color)', className: 'w2' }; | ||
| default:return { fontSize: 24, lineHeight: 24*1.18, maxWidth: 360, color: 'var(--w1-color)', className: 'w1' }; | ||
| } | ||
| } |
| for (let qi = 0; qi < sorted.length; qi++) { | ||
| const q = sorted[qi]; | ||
| const p = paramsForWeight(q.weight); | ||
| const m = measureMultilineText(q.text, p.fontSize, 700, p.maxWidth); |
| */ | ||
| function measureMultilineText(text, fontSize, fontWeight, maxWidth) { | ||
| setMeasureFont(fontSize, fontWeight); | ||
| const lineHeight = Math.round(fontSize * 1.18); |
| window.addEventListener('resize', () => { | ||
| fitPaperToStage(); | ||
| }); |
| display: inline-block; | ||
| width: 36px; height: 20px; | ||
| } | ||
| .switch input { display: none; } |
| ### Requirement: Canvas 多行文本测量 | ||
| 系统 SHALL 在内存中创建隐藏的离屏 `<canvas>` 元素,使用 `context.measureText()` 测量文本尺寸。 | ||
| 系统 MUST 提供 `measureMultilineText(text, fontSize, fontWeight, maxWidth)` 函数,返回 `[width, height]`。 | ||
| 系统 MUST 在 Canvas 虚拟环境中模拟 word-wrapping(按词或字符切分),按 `lineHeight = fontSize * 1.2` 累计高度。 | ||
| 不同 `weight` 应对应不同 `fontSize`(weight 3 最大)和不同 `maxWidth`(大字号对应更宽的 maxWidth)。 |
| - [x] Task 2: 实现 Canvas 多行文本测量 | ||
| - [x] SubTask 2.1: 创建离屏 `<canvas>` 实例,封装 `measureMultilineText(text, fontSize, fontWeight, maxWidth)` 返回 `[width, height]` | ||
| - [x] SubTask 2.2: 按 `weight` 映射 fontSize 与 maxWidth(weight 3 → 最大字号 + 较宽 maxWidth) |
| - [x] 使用离屏 `<canvas>` + `context.measureText()` 测量文本,不使用 `offsetWidth/offsetHeight` | ||
| - [x] `measureMultilineText(text, fontSize, fontWeight, maxWidth)` 函数签名与返回值正确 | ||
| - [x] 不同 weight 映射不同 fontSize 与 maxWidth(weight 3 最大 + 较宽) |
🎯 Changes
1. 新增 AI 驱动句子云海报生成器
index.html文件,包含一个完整的单文件 HTML 应用,实现 AI 驱动的句子云海报生成功能。google/gemini-flash-1.5进行 AI 权重请求,并提供本地启发式回退机制,增强鲁棒性。@page规则,优化浏览器原生打印管线。2. 新增配套文档
spec.md规格文档,详细定义了 AI 权重分析、Canvas 多行文本测量、阿基米德螺旋线布局、画板比例与等比缩放、控制面板与打印、Hover 聚焦交互、作者显示策略七大核心能力的需求与场景。tasks.md任务清单,按 8 个 Task 拆解了 HTML 骨架、文本测量、螺旋布局、缩放、控制面板/打印、交互、AI 容错与集成验证的实现步骤,并标注了任务依赖关系。checklist.md验收清单,逐项核对单文件可运行性、API Key 占位、测量函数签名、权重排序、螺旋线步进、AABB 零重叠、纸张比例、打印样式、Loading 态等交付要点。💡 Technical Highlights
@page规则,充分利用浏览器打印能力,提供高质量打印输出。