@amaster.ai/pi-lark 0.1.6 → 0.1.7

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.
Files changed (178) hide show
  1. package/README.md +5 -1
  2. package/dist/config.d.ts +1 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +2 -2
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/package.json +3 -3
  10. package/skills/lark-apps/SKILL.md +24 -12
  11. package/skills/lark-apps/creative-design/agents/assets/vision-probe.png +0 -0
  12. package/skills/lark-apps/creative-design/agents/fork-verifier-agent.md +71 -0
  13. package/skills/lark-apps/creative-design/agents/vision-probe-agent.md +41 -0
  14. package/skills/lark-apps/creative-design/assets/index.html +27 -0
  15. package/skills/lark-apps/creative-design/creative-design.md +239 -0
  16. package/skills/lark-apps/creative-design/references/aily.md +39 -0
  17. package/skills/lark-apps/creative-design/references/animated-video.md +34 -0
  18. package/skills/lark-apps/creative-design/references/charts.md +165 -0
  19. package/skills/lark-apps/creative-design/references/claude.md +36 -0
  20. package/skills/lark-apps/creative-design/references/codex.md +32 -0
  21. package/skills/lark-apps/creative-design/references/data-report.md +108 -0
  22. package/skills/lark-apps/creative-design/references/frontend-design.md +71 -0
  23. package/skills/lark-apps/creative-design/references/hi-fi-design.md +32 -0
  24. package/skills/lark-apps/creative-design/references/interactive-prototype.md +24 -0
  25. package/skills/lark-apps/creative-design/references/make-a-deck.md +133 -0
  26. package/skills/lark-apps/creative-design/references/visual-exposure.md +82 -0
  27. package/skills/lark-apps/creative-design/references/wireframe.md +14 -0
  28. package/skills/lark-apps/creative-design/starter-components/android-frame.jsx +188 -0
  29. package/skills/lark-apps/creative-design/starter-components/animations.jsx +773 -0
  30. package/skills/lark-apps/creative-design/starter-components/browser-window.jsx +122 -0
  31. package/skills/lark-apps/creative-design/starter-components/deck-stage.js +2483 -0
  32. package/skills/lark-apps/creative-design/starter-components/design-canvas.jsx +1432 -0
  33. package/skills/lark-apps/creative-design/starter-components/ios-frame.jsx +270 -0
  34. package/skills/lark-apps/creative-design/starter-components/macos-window.jsx +197 -0
  35. package/skills/lark-apps/creative-design/starter-components/tweaks-panel.jsx +752 -0
  36. package/skills/lark-apps/references/lark-apps-automation.md +80 -2
  37. package/skills/lark-apps/references/lark-apps-cache.md +61 -0
  38. package/skills/lark-apps/references/lark-apps-cloud-dev.md +0 -1
  39. package/skills/lark-apps/references/lark-apps-create.md +1 -2
  40. package/skills/lark-apps/references/lark-apps-db.md +1 -1
  41. package/skills/lark-apps/references/lark-apps-env-pull.md +1 -1
  42. package/skills/lark-apps/references/lark-apps-file.md +2 -2
  43. package/skills/lark-apps/references/lark-apps-git-credential.md +1 -1
  44. package/skills/lark-apps/references/lark-apps-html-publish.md +4 -8
  45. package/skills/lark-apps/references/lark-apps-init.md +1 -1
  46. package/skills/lark-apps/references/lark-apps-list.md +1 -1
  47. package/skills/lark-apps/references/lark-apps-local-dev.md +54 -11
  48. package/skills/lark-apps/references/lark-apps-openapi-key.md +1 -1
  49. package/skills/lark-apps/references/lark-apps-release-create.md +2 -2
  50. package/skills/lark-apps/references/lark-apps-release-get.md +3 -3
  51. package/skills/lark-base/SKILL.md +20 -13
  52. package/skills/lark-base/references/lark-base-cell-value.md +3 -3
  53. package/skills/lark-base/references/lark-base-data-query.md +11 -4
  54. package/skills/lark-base/references/lark-base-field-create.md +4 -0
  55. package/skills/lark-base/references/lark-base-field-json.md +4 -4
  56. package/skills/lark-base/references/lark-base-field-update.md +17 -1
  57. package/skills/lark-base/references/lark-base-filter-condition.md +179 -0
  58. package/skills/lark-base/references/lark-base-form-questions-create.md +40 -7
  59. package/skills/lark-base/references/lark-base-form-questions-update.md +73 -20
  60. package/skills/lark-base/references/lark-base-form-submit.md +16 -7
  61. package/skills/lark-base/references/lark-base-record-batch-create.md +12 -10
  62. package/skills/lark-base/references/lark-base-record-batch-update.md +11 -9
  63. package/skills/lark-base/references/lark-base-record-upsert.md +1 -1
  64. package/skills/lark-base/references/lark-base-role-guide.md +11 -0
  65. package/skills/lark-base/references/lark-base-view-set-filter.md +11 -137
  66. package/skills/lark-base/references/role-config.md +31 -5
  67. package/skills/lark-calendar/SKILL.md +14 -8
  68. package/skills/lark-calendar/references/lark-calendar-create.md +6 -5
  69. package/skills/lark-calendar/references/lark-calendar-recurring.md +1 -0
  70. package/skills/lark-calendar/references/lark-calendar-room-find.md +2 -1
  71. package/skills/lark-calendar/references/lark-calendar-schedule-clear-time.md +1 -0
  72. package/skills/lark-calendar/references/lark-calendar-suggestion.md +1 -1
  73. package/skills/lark-calendar/references/lark-calendar-update.md +10 -4
  74. package/skills/lark-contact/SKILL.md +19 -3
  75. package/skills/lark-contact/references/lark-contact-search-bot.md +60 -0
  76. package/skills/lark-doc/references/lark-doc-fetch.md +10 -2
  77. package/skills/lark-doc/references/lark-doc-whiteboard.md +9 -8
  78. package/skills/lark-doc/references/lark-doc-xml-extended-blocks.md +41 -0
  79. package/skills/lark-doc/references/lark-doc-xml.md +4 -3
  80. package/skills/lark-drive/SKILL.md +25 -45
  81. package/skills/lark-drive/references/lark-drive-add-comment.md +2 -4
  82. package/skills/lark-drive/references/lark-drive-add-reply.md +47 -0
  83. package/skills/lark-drive/references/lark-drive-apply-permission.md +2 -2
  84. package/skills/lark-drive/references/lark-drive-batch-query-comments.md +46 -0
  85. package/skills/lark-drive/references/lark-drive-comment-content.md +50 -0
  86. package/skills/lark-drive/references/lark-drive-comment-location.md +9 -15
  87. package/skills/lark-drive/references/lark-drive-delete-reply.md +48 -0
  88. package/skills/lark-drive/references/lark-drive-download.md +5 -1
  89. package/skills/lark-drive/references/lark-drive-list-comments.md +25 -68
  90. package/skills/lark-drive/references/lark-drive-list-replies.md +54 -0
  91. package/skills/lark-drive/references/lark-drive-member-add.md +2 -2
  92. package/skills/lark-drive/references/lark-drive-member-list.md +65 -0
  93. package/skills/lark-drive/references/lark-drive-permission-get-setting.md +48 -0
  94. package/skills/lark-drive/references/lark-drive-preview.md +11 -1
  95. package/skills/lark-drive/references/lark-drive-react-reply.md +51 -0
  96. package/skills/lark-drive/references/lark-drive-reactions.md +27 -25
  97. package/skills/lark-drive/references/lark-drive-resolve-comment.md +45 -0
  98. package/skills/lark-drive/references/lark-drive-restore-comment.md +46 -0
  99. package/skills/lark-drive/references/lark-drive-search.md +7 -1
  100. package/skills/lark-drive/references/lark-drive-secure-label.md +1 -1
  101. package/skills/lark-drive/references/lark-drive-update-reply.md +46 -0
  102. package/skills/lark-drive/references/lark-drive-upload.md +1 -0
  103. package/skills/lark-drive/references/lark-drive-workflow-permission-governance-commands.md +38 -8
  104. package/skills/lark-drive/references/lark-drive-workflow-permission-governance-outputs.md +10 -10
  105. package/skills/lark-drive/references/lark-drive-workflow-permission-governance.md +22 -20
  106. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-execute.md +273 -0
  107. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-recall.md +202 -0
  108. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-resolve-verify.md +231 -0
  109. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-review-plan.md +248 -0
  110. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-setup.md +174 -0
  111. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector.md +202 -0
  112. package/skills/lark-drive/references/lark-drive-workflow.md +5 -4
  113. package/skills/lark-event/SKILL.md +1 -0
  114. package/skills/lark-event/references/lark-event-application.md +38 -0
  115. package/skills/lark-im/SKILL.md +1 -1
  116. package/skills/lark-im/references/card/card-2.0-schema.md +1 -1
  117. package/skills/lark-im/references/card/lark-im-card-style.md +4 -4
  118. package/skills/lark-im/references/card/resource/icons.md +14 -0
  119. package/skills/lark-im/references/lark-im-flag-list.md +8 -7
  120. package/skills/lark-okr/SKILL.md +71 -26
  121. package/skills/lark-okr/references/lark-okr-batch-create.md +19 -18
  122. package/skills/lark-okr/references/lark-okr-create.md +173 -0
  123. package/skills/lark-okr/references/lark-okr-cycle-list.md +17 -7
  124. package/skills/lark-okr/references/lark-okr-entities.md +1 -0
  125. package/skills/lark-okr/references/lark-okr-indicator-update.md +3 -1
  126. package/skills/lark-okr/references/lark-okr-indicators.md +61 -12
  127. package/skills/lark-okr/references/lark-okr-progress-list.md +21 -9
  128. package/skills/lark-slides/SKILL.md +115 -68
  129. package/skills/lark-slides/references/asset-planning.md +6 -4
  130. package/skills/lark-slides/references/iconpark.md +2 -2
  131. package/skills/lark-slides/references/lark-slides-create.md +16 -8
  132. package/skills/lark-slides/references/lark-slides-history.md +132 -0
  133. package/skills/lark-slides/references/lark-slides-media-upload.md +2 -3
  134. package/skills/lark-slides/references/lark-slides-pptx-template-workflows.md +7 -11
  135. package/skills/lark-slides/references/lark-slides-replace-slide.md +0 -3
  136. package/skills/lark-slides/references/lark-slides-screenshot.md +4 -4
  137. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-create.md +219 -0
  138. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-delete.md +6 -5
  139. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-get.md +2 -2
  140. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-replace.md +2 -3
  141. package/skills/lark-slides/references/lark-slides-xml-presentations-get.md +65 -30
  142. package/skills/lark-slides/references/planning-layer.md +11 -10
  143. package/skills/lark-slides/references/slides_chart_demo.xml +1416 -1
  144. package/skills/lark-slides/references/slides_xml_schema_definition.xml +492 -76
  145. package/skills/lark-slides/references/troubleshooting.md +25 -7
  146. package/skills/lark-slides/references/validation-checklist.md +53 -16
  147. package/skills/lark-slides/references/visual-planning.md +25 -22
  148. package/skills/lark-slides/references/xml-schema-quick-ref.md +281 -45
  149. package/skills/lark-slides/scripts/sxsd_validator.py +908 -0
  150. package/skills/lark-slides/scripts/xml_text_overlap_lint.py +1650 -165
  151. package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +3139 -513
  152. package/skills/lark-task/SKILL.md +7 -0
  153. package/skills/lark-task/references/lark-task-complete.md +6 -2
  154. package/skills/lark-task/references/lark-task-create.md +9 -0
  155. package/skills/lark-task/references/lark-task-update.md +6 -2
  156. package/skills/lark-whiteboard/SKILL.md +13 -12
  157. package/skills/lark-whiteboard/elements/layout.md +1 -1
  158. package/skills/lark-whiteboard/elements/schema.md +2 -2
  159. package/skills/lark-whiteboard/references/{lark-whiteboard-query.md → lark-whiteboard-export.md} +15 -15
  160. package/skills/lark-whiteboard/references/lark-whiteboard-update.md +3 -3
  161. package/skills/lark-whiteboard/references/lark-whiteboard-workflow.md +7 -17
  162. package/skills/lark-whiteboard/routes/dsl.md +3 -3
  163. package/skills/lark-whiteboard/routes/mermaid.md +2 -2
  164. package/skills/lark-whiteboard/routes/svg-edit.md +4 -4
  165. package/skills/lark-whiteboard/routes/svg.md +11 -6
  166. package/skills/lark-whiteboard/scenes/bar-chart.md +1 -1
  167. package/skills/lark-whiteboard/scenes/fishbone.md +1 -1
  168. package/skills/lark-whiteboard/scenes/flywheel.md +1 -1
  169. package/skills/lark-whiteboard/scenes/line-chart.md +1 -1
  170. package/skills/lark-whiteboard/scenes/treemap.md +1 -1
  171. package/skills/lark-wiki/SKILL.md +1 -0
  172. package/skills/lark-drive/references/lark-drive-comments-guide.md +0 -80
  173. package/skills/lark-slides/references/examples.md +0 -91
  174. package/skills/lark-slides/references/lark-slides-whiteboard.md +0 -331
  175. package/skills/lark-slides/references/lark-slides-xml-get.md +0 -100
  176. package/skills/lark-slides/references/slide-templates.md +0 -201
  177. package/skills/lark-slides/references/slides_demo.xml +0 -226
  178. package/skills/lark-slides/references/xml-format-guide.md +0 -433
@@ -0,0 +1,773 @@
1
+ /* BEGIN USAGE */
2
+ // animations.jsx
3
+ // Reusable animation starter: Stage, Timeline, Sprite, easing helpers.
4
+ // Exports (to window): Stage, Sprite, PlaybackBar, TextSprite, ImageSprite, RectSprite,
5
+ // useTime, useTimeline, useSprite, Easing, interpolate, animate, clamp.
6
+ //
7
+ // Usage (in an HTML file that loads React + Babel):
8
+ //
9
+ // <Stage width={1280} height={720} duration={10} background="#f6f4ef">
10
+ // <MyScene />
11
+ // </Stage>
12
+ //
13
+ // <Stage> auto-scales to the viewport and provides the scrubber, play/pause,
14
+ // ←/→ seek, space, and 0-to-reset controls, and persists the playhead.
15
+ // Set the optional `poster` prop (seconds) to the moment your opening scene is
16
+ // fully composed — the product thumbnail freezes on that frame (default ~1s).
17
+ // Inside <Stage>, any child can call useTime() to read the current
18
+ // playhead (seconds). Or wrap content in <Sprite start={1} end={4}>...</Sprite>
19
+ // to only render during that window -- children receive a `localTime` and
20
+ // `progress` via the useSprite() hook. Use Easing + interpolate()/animate()
21
+ // for tweens; TextSprite / ImageSprite / RectSprite have built-in entry/exit.
22
+ // Build YOUR scenes by composing Sprites inside a Stage.
23
+ /* END USAGE */
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+
26
+ // ── Easing functions (hand-rolled, Popmotion-style) ─────────────────────────
27
+ // All easings take t ∈ [0,1] and return eased t ∈ [0,1] (may overshoot for back/elastic).
28
+ const Easing = {
29
+ linear: (t) => t,
30
+
31
+ // Quad
32
+ easeInQuad: (t) => t * t,
33
+ easeOutQuad: (t) => t * (2 - t),
34
+ easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
35
+
36
+ // Cubic
37
+ easeInCubic: (t) => t * t * t,
38
+ easeOutCubic: (t) => (--t) * t * t + 1,
39
+ easeInOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1),
40
+
41
+ // Quart
42
+ easeInQuart: (t) => t * t * t * t,
43
+ easeOutQuart: (t) => 1 - (--t) * t * t * t,
44
+ easeInOutQuart: (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t),
45
+
46
+ // Expo
47
+ easeInExpo: (t) => (t === 0 ? 0 : Math.pow(2, 10 * (t - 1))),
48
+ easeOutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
49
+ easeInOutExpo: (t) => {
50
+ if (t === 0) return 0;
51
+ if (t === 1) return 1;
52
+ if (t < 0.5) return 0.5 * Math.pow(2, 20 * t - 10);
53
+ return 1 - 0.5 * Math.pow(2, -20 * t + 10);
54
+ },
55
+
56
+ // Sine
57
+ easeInSine: (t) => 1 - Math.cos((t * Math.PI) / 2),
58
+ easeOutSine: (t) => Math.sin((t * Math.PI) / 2),
59
+ easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
60
+
61
+ // Back (overshoot)
62
+ easeOutBack: (t) => {
63
+ const c1 = 1.70158, c3 = c1 + 1;
64
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
65
+ },
66
+ easeInBack: (t) => {
67
+ const c1 = 1.70158, c3 = c1 + 1;
68
+ return c3 * t * t * t - c1 * t * t;
69
+ },
70
+ easeInOutBack: (t) => {
71
+ const c1 = 1.70158, c2 = c1 * 1.525;
72
+ return t < 0.5
73
+ ? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
74
+ : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
75
+ },
76
+
77
+ // Elastic
78
+ easeOutElastic: (t) => {
79
+ const c4 = (2 * Math.PI) / 3;
80
+ if (t === 0) return 0;
81
+ if (t === 1) return 1;
82
+ return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
83
+ },
84
+ };
85
+
86
+ // ── Core interpolation helpers ──────────────────────────────────────────────
87
+
88
+ // Clamp a value to [min, max]
89
+ const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
90
+
91
+ // interpolate([0, 0.5, 1], [0, 100, 50], ease?) -> fn(t)
92
+ // Popmotion-style: linearly maps t across input keyframes to output values,
93
+ // with optional easing per segment (single fn or array of fns).
94
+ function interpolate(input, output, ease = Easing.linear) {
95
+ return (t) => {
96
+ if (t <= input[0]) return output[0];
97
+ if (t >= input[input.length - 1]) return output[output.length - 1];
98
+ for (let i = 0; i < input.length - 1; i++) {
99
+ if (t >= input[i] && t <= input[i + 1]) {
100
+ const span = input[i + 1] - input[i];
101
+ const local = span === 0 ? 0 : (t - input[i]) / span;
102
+ const easeFn = Array.isArray(ease) ? (ease[i] || Easing.linear) : ease;
103
+ const eased = easeFn(local);
104
+ return output[i] + (output[i + 1] - output[i]) * eased;
105
+ }
106
+ }
107
+ return output[output.length - 1];
108
+ };
109
+ }
110
+
111
+ // animate({from, to, start, end, ease})(t) — simpler single-segment tween.
112
+ // Returns `from` before `start`, `to` after `end`.
113
+ function animate({ from = 0, to = 1, start = 0, end = 1, ease = Easing.easeInOutCubic }) {
114
+ return (t) => {
115
+ if (t <= start) return from;
116
+ if (t >= end) return to;
117
+ const local = (t - start) / (end - start);
118
+ return from + (to - from) * ease(local);
119
+ };
120
+ }
121
+
122
+ // ── Timeline context ────────────────────────────────────────────────────────
123
+
124
+ const TimelineContext = React.createContext({ time: 0, duration: 10, playing: false });
125
+
126
+ const useTime = () => React.useContext(TimelineContext).time;
127
+ const useTimeline = () => React.useContext(TimelineContext);
128
+
129
+ // ── Sprite ──────────────────────────────────────────────────────────────────
130
+ // Renders children only when the playhead is inside [start, end]. Provides
131
+ // a sub-context with `localTime` (seconds since start) and `progress` (0..1).
132
+ //
133
+ // <Sprite start={2} end={5}>
134
+ // {({ localTime, progress }) => <Thing x={progress * 100} />}
135
+ // </Sprite>
136
+ //
137
+ // Or as a plain wrapper — children can call useSprite() themselves.
138
+
139
+ const SpriteContext = React.createContext({ localTime: 0, progress: 0, duration: 0 });
140
+ const useSprite = () => React.useContext(SpriteContext);
141
+
142
+ function Sprite({ start = 0, end = Infinity, children, keepMounted = false }) {
143
+ const { time } = useTimeline();
144
+ const visible = time >= start && time <= end;
145
+ if (!visible && !keepMounted) return null;
146
+
147
+ const duration = end - start;
148
+ const localTime = Math.max(0, time - start);
149
+ const progress = duration > 0 && isFinite(duration)
150
+ ? clamp(localTime / duration, 0, 1)
151
+ : 0;
152
+
153
+ const value = { localTime, progress, duration, visible };
154
+
155
+ return (
156
+ <SpriteContext.Provider value={value}>
157
+ {typeof children === 'function' ? children(value) : children}
158
+ </SpriteContext.Provider>
159
+ );
160
+ }
161
+
162
+ // ── Sample sprite components ────────────────────────────────────────────────
163
+
164
+ // TextSprite: fades/slides text in on entry, holds, then fades out on exit.
165
+ // Props: text, x, y, size, color, font, entryDur, exitDur, align
166
+ function TextSprite({
167
+ text,
168
+ x = 0, y = 0,
169
+ size = 48,
170
+ color = '#111',
171
+ font = 'Inter, system-ui, sans-serif',
172
+ weight = 600,
173
+ entryDur = 0.45,
174
+ exitDur = 0.35,
175
+ entryEase = Easing.easeOutBack,
176
+ exitEase = Easing.easeInCubic,
177
+ align = 'left',
178
+ letterSpacing = '-0.01em',
179
+ }) {
180
+ const { localTime, duration } = useSprite();
181
+ const exitStart = Math.max(0, duration - exitDur);
182
+
183
+ let opacity = 1;
184
+ let ty = 0;
185
+
186
+ if (localTime < entryDur) {
187
+ const t = entryEase(clamp(localTime / entryDur, 0, 1));
188
+ opacity = t;
189
+ ty = (1 - t) * 16;
190
+ } else if (localTime > exitStart) {
191
+ const t = exitEase(clamp((localTime - exitStart) / exitDur, 0, 1));
192
+ opacity = 1 - t;
193
+ ty = -t * 8;
194
+ }
195
+
196
+ const translateX = align === 'center' ? '-50%' : align === 'right' ? '-100%' : '0';
197
+
198
+ return (
199
+ <div style={{
200
+ position: 'absolute',
201
+ left: x, top: y,
202
+ transform: `translate(${translateX}, ${ty}px)`,
203
+ opacity,
204
+ fontFamily: font,
205
+ fontSize: size,
206
+ fontWeight: weight,
207
+ color,
208
+ letterSpacing,
209
+ whiteSpace: 'pre',
210
+ lineHeight: 1.1,
211
+ willChange: 'transform, opacity',
212
+ }}>
213
+ {text}
214
+ </div>
215
+ );
216
+ }
217
+
218
+ // ImageSprite: scales + fades in; optional Ken Burns drift during hold.
219
+ function ImageSprite({
220
+ src,
221
+ x = 0, y = 0,
222
+ width = 400, height = 300,
223
+ entryDur = 0.6,
224
+ exitDur = 0.4,
225
+ kenBurns = false,
226
+ kenBurnsScale = 1.08,
227
+ radius = 12,
228
+ fit = 'cover',
229
+ placeholder = null, // {label: string} for striped placeholder
230
+ }) {
231
+ const { localTime, duration } = useSprite();
232
+ const exitStart = Math.max(0, duration - exitDur);
233
+
234
+ let opacity = 1;
235
+ let scale = 1;
236
+
237
+ if (localTime < entryDur) {
238
+ const t = Easing.easeOutCubic(clamp(localTime / entryDur, 0, 1));
239
+ opacity = t;
240
+ scale = 0.96 + 0.04 * t;
241
+ } else if (localTime > exitStart) {
242
+ const t = Easing.easeInCubic(clamp((localTime - exitStart) / exitDur, 0, 1));
243
+ opacity = 1 - t;
244
+ scale = (kenBurns ? kenBurnsScale : 1) + 0.02 * t;
245
+ } else if (kenBurns) {
246
+ const holdSpan = exitStart - entryDur;
247
+ const holdT = holdSpan > 0 ? (localTime - entryDur) / holdSpan : 0;
248
+ scale = 1 + (kenBurnsScale - 1) * holdT;
249
+ }
250
+
251
+ const content = placeholder ? (
252
+ <div style={{
253
+ width: '100%', height: '100%',
254
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
255
+ background: 'repeating-linear-gradient(135deg, #e9e6df 0 10px, #dcd8cf 10px 20px)',
256
+ color: '#6b6458',
257
+ fontFamily: 'JetBrains Mono, ui-monospace, monospace',
258
+ fontSize: 13,
259
+ letterSpacing: '0.04em',
260
+ textTransform: 'uppercase',
261
+ }}>
262
+ {placeholder.label || 'image'}
263
+ </div>
264
+ ) : (
265
+ <img src={src} alt="" style={{ width: '100%', height: '100%', objectFit: fit, display: 'block' }} />
266
+ );
267
+
268
+ return (
269
+ <div style={{
270
+ position: 'absolute',
271
+ left: x, top: y,
272
+ width, height,
273
+ opacity,
274
+ transform: `scale(${scale})`,
275
+ transformOrigin: 'center',
276
+ borderRadius: radius,
277
+ overflow: 'hidden',
278
+ willChange: 'transform, opacity',
279
+ }}>
280
+ {content}
281
+ </div>
282
+ );
283
+ }
284
+
285
+ // RectSprite: simple rectangle that animates position/size/color via props.
286
+ // Useful demo primitive — takes a `render` fn for per-frame customization.
287
+ function RectSprite({
288
+ x = 0, y = 0,
289
+ width = 100, height = 100,
290
+ color = '#111',
291
+ radius = 8,
292
+ entryDur = 0.4,
293
+ exitDur = 0.3,
294
+ render, // optional: (ctx) => style overrides
295
+ }) {
296
+ const spriteCtx = useSprite();
297
+ const { localTime, duration } = spriteCtx;
298
+ const exitStart = Math.max(0, duration - exitDur);
299
+
300
+ let opacity = 1;
301
+ let scale = 1;
302
+
303
+ if (localTime < entryDur) {
304
+ const t = Easing.easeOutBack(clamp(localTime / entryDur, 0, 1));
305
+ opacity = clamp(localTime / entryDur, 0, 1);
306
+ scale = 0.4 + 0.6 * t;
307
+ } else if (localTime > exitStart) {
308
+ const t = Easing.easeInQuad(clamp((localTime - exitStart) / exitDur, 0, 1));
309
+ opacity = 1 - t;
310
+ scale = 1 - 0.15 * t;
311
+ }
312
+
313
+ const overrides = render ? render(spriteCtx) : {};
314
+
315
+ return (
316
+ <div style={{
317
+ position: 'absolute',
318
+ left: x, top: y,
319
+ width, height,
320
+ background: color,
321
+ borderRadius: radius,
322
+ opacity,
323
+ transform: `scale(${scale})`,
324
+ transformOrigin: 'center',
325
+ willChange: 'transform, opacity',
326
+ ...overrides,
327
+ }} />
328
+ );
329
+ }
330
+
331
+
332
+ function Stage({
333
+ width = 1280,
334
+ height = 720,
335
+ duration = 10,
336
+ background = '#f6f4ef',
337
+ fps = 60,
338
+ loop = true,
339
+ autoplay = true,
340
+ poster = null,
341
+ persistKey = 'animstage',
342
+ children,
343
+ }) {
344
+ // Thumbnail capture mode: the host appends ?thumbnail=1 before screenshotting.
345
+ // Freeze on a representative still — the author-declared `poster` second, or
346
+ // ~1s as a fallback — paused, with the playback bar hidden, so the product
347
+ // thumbnail is a deterministic frame of the first composed scene.
348
+ const captureMode = typeof location !== 'undefined' && /[?&]thumbnail=/.test(location.search || '');
349
+
350
+ const [time, setTime] = React.useState(() => {
351
+ if (captureMode) return clamp(poster == null ? 1 : poster, 0, duration);
352
+ try {
353
+ const v = parseFloat(localStorage.getItem(persistKey + ':t') || '0');
354
+ return isFinite(v) ? clamp(v, 0, duration) : 0;
355
+ } catch { return 0; }
356
+ });
357
+ const [playing, setPlaying] = React.useState(captureMode ? false : autoplay);
358
+ const [scale, setScale] = React.useState(1);
359
+
360
+ const stageRef = React.useRef(null);
361
+ const canvasRef = React.useRef(null);
362
+ const rafRef = React.useRef(null);
363
+ const lastTsRef = React.useRef(null);
364
+
365
+ // Persist playhead
366
+ React.useEffect(() => {
367
+ try { localStorage.setItem(persistKey + ':t', String(time)); } catch {}
368
+ }, [time, persistKey]);
369
+
370
+ // Auto-scale to fit viewport
371
+ React.useEffect(() => {
372
+ if (!stageRef.current) return;
373
+ const el = stageRef.current;
374
+ const measure = () => {
375
+ const barH = captureMode ? 0 : 44; // playback bar height (hidden in capture mode)
376
+ const s = Math.min(
377
+ el.clientWidth / width,
378
+ (el.clientHeight - barH) / height
379
+ );
380
+ setScale(Math.max(0.05, s));
381
+ };
382
+ measure();
383
+ const ro = new ResizeObserver(measure);
384
+ ro.observe(el);
385
+ window.addEventListener('resize', measure);
386
+ return () => {
387
+ ro.disconnect();
388
+ window.removeEventListener('resize', measure);
389
+ };
390
+ }, [width, height]);
391
+
392
+ // Animation loop
393
+ React.useEffect(() => {
394
+ if (!playing) {
395
+ lastTsRef.current = null;
396
+ return;
397
+ }
398
+ const step = (ts) => {
399
+ if (lastTsRef.current == null) lastTsRef.current = ts;
400
+ const dt = (ts - lastTsRef.current) / 1000;
401
+ lastTsRef.current = ts;
402
+ setTime((t) => {
403
+ let next = t + dt;
404
+ if (next >= duration) {
405
+ if (loop) next = next % duration;
406
+ else { next = duration; setPlaying(false); }
407
+ }
408
+ return next;
409
+ });
410
+ rafRef.current = requestAnimationFrame(step);
411
+ };
412
+ rafRef.current = requestAnimationFrame(step);
413
+ return () => {
414
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
415
+ lastTsRef.current = null;
416
+ };
417
+ }, [playing, duration, loop]);
418
+
419
+ // Keyboard: space = play/pause, ← → = seek
420
+ React.useEffect(() => {
421
+ const onKey = (e) => {
422
+ if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) return;
423
+ if (e.code === 'Space') {
424
+ e.preventDefault();
425
+ setPlaying(p => !p);
426
+ } else if (e.code === 'ArrowLeft') {
427
+ setTime(t => clamp(t - (e.shiftKey ? 1 : 0.1), 0, duration));
428
+ } else if (e.code === 'ArrowRight') {
429
+ setTime(t => clamp(t + (e.shiftKey ? 1 : 0.1), 0, duration));
430
+ } else if (e.key === '0' || e.code === 'Home') {
431
+ setTime(0);
432
+ }
433
+ };
434
+ window.addEventListener('keydown', onKey);
435
+ return () => window.removeEventListener('keydown', onKey);
436
+ }, [duration]);
437
+
438
+ const ctxValue = React.useMemo(
439
+ () => ({ time, duration, playing, setTime, setPlaying }),
440
+ [time, duration, playing]
441
+ );
442
+
443
+ return (
444
+ <div
445
+ ref={stageRef}
446
+ style={{
447
+ position: 'absolute', inset: 0,
448
+ display: 'flex', flexDirection: 'column',
449
+ alignItems: 'center',
450
+ background: '#0a0a0a',
451
+ fontFamily: 'Inter, system-ui, sans-serif',
452
+ }}
453
+ >
454
+ {/* Canvas area — vertically centered in remaining space */}
455
+ <div style={{
456
+ flex: 1,
457
+ width: '100%',
458
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
459
+ overflow: 'hidden',
460
+ minHeight: 0,
461
+ }}>
462
+ <div
463
+ ref={canvasRef}
464
+ style={{
465
+ width, height,
466
+ background,
467
+ position: 'relative',
468
+ transform: `scale(${scale})`,
469
+ transformOrigin: 'center',
470
+ flexShrink: 0,
471
+ boxShadow: '0 20px 60px rgba(0,0,0,0.4)',
472
+ overflow: 'hidden',
473
+ }}
474
+ >
475
+ <TimelineContext.Provider value={ctxValue}>
476
+ {children}
477
+ </TimelineContext.Provider>
478
+ </div>
479
+ </div>
480
+
481
+ {/* Playback bar — stacked below canvas, never overlapping. Hidden in
482
+ capture mode so the thumbnail is just the frame, no chrome. */}
483
+ {!captureMode && (
484
+ <PlaybackBar
485
+ time={time}
486
+ duration={duration}
487
+ playing={playing}
488
+ onPlayPause={() => setPlaying(p => !p)}
489
+ onReset={() => { setTime(0); }}
490
+ onSeek={(t) => setTime(t)}
491
+ />
492
+ )}
493
+ </div>
494
+ );
495
+ }
496
+
497
+ // ── Playback bar ────────────────────────────────────────────────────────────
498
+ // Play/pause, return-to-begin, scrub track, time display.
499
+ // Uses fixed-width time fields so layout doesn't thrash.
500
+
501
+ function PlaybackBar({ time, duration, playing, onPlayPause, onReset, onSeek }) {
502
+ const trackRef = React.useRef(null);
503
+ const [dragging, setDragging] = React.useState(false);
504
+ const [trackHover, setTrackHover] = React.useState(null); // { x, t } — px within track + hovered time
505
+
506
+ const posFromEvent = React.useCallback((e) => {
507
+ const rect = trackRef.current.getBoundingClientRect();
508
+ const x = clamp(e.clientX - rect.left, 0, rect.width);
509
+ const t = rect.width > 0 ? (x / rect.width) * duration : 0;
510
+ return { x, t };
511
+ }, [duration]);
512
+
513
+ const onTrackMove = (e) => {
514
+ if (!trackRef.current) return;
515
+ const { x, t } = posFromEvent(e);
516
+ setTrackHover({ x, t });
517
+ if (dragging) onSeek(t);
518
+ };
519
+
520
+ const onTrackLeave = () => {
521
+ if (!dragging) setTrackHover(null);
522
+ };
523
+
524
+ const onTrackDown = (e) => {
525
+ const { x, t } = posFromEvent(e);
526
+ setDragging(true);
527
+ setTrackHover({ x, t });
528
+ onSeek(t);
529
+ };
530
+
531
+ // Grab the knob in place: begin dragging without seeking (no jump).
532
+ // stopPropagation keeps the track's click-to-seek from also firing.
533
+ const onBallDown = (e) => {
534
+ e.stopPropagation();
535
+ setDragging(true);
536
+ setTrackHover(posFromEvent(e));
537
+ };
538
+
539
+ React.useEffect(() => {
540
+ if (!dragging) return;
541
+ const prevCursor = document.body.style.cursor;
542
+ document.body.style.cursor = 'grabbing'; // stays grabbing even if the pointer leaves the knob mid-drag
543
+ const onUp = () => {
544
+ setDragging(false);
545
+ setTrackHover(null);
546
+ };
547
+ const onMove = (e) => {
548
+ if (!trackRef.current) return;
549
+ const { x, t } = posFromEvent(e);
550
+ setTrackHover({ x, t });
551
+ onSeek(t);
552
+ };
553
+ window.addEventListener('mouseup', onUp);
554
+ window.addEventListener('mousemove', onMove);
555
+ return () => {
556
+ window.removeEventListener('mouseup', onUp);
557
+ window.removeEventListener('mousemove', onMove);
558
+ document.body.style.cursor = prevCursor;
559
+ };
560
+ }, [dragging, posFromEvent, onSeek]);
561
+
562
+ const pct = duration > 0 ? (time / duration) * 100 : 0;
563
+ const fmt = (t) => {
564
+ const total = Math.max(0, t);
565
+ const m = Math.floor(total / 60);
566
+ const s = Math.floor(total % 60);
567
+ return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
568
+ };
569
+
570
+ const numFont = '"PingFang SC", -apple-system, BlinkMacSystemFont, system-ui, sans-serif';
571
+
572
+ return (
573
+ <div style={{
574
+ display: 'flex', alignItems: 'center', gap: 12,
575
+ padding: '12px',
576
+ background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%)',
577
+ width: '100%',
578
+ color: '#fff',
579
+ fontFamily: numFont,
580
+ userSelect: 'none',
581
+ flexShrink: 0,
582
+ boxSizing: 'border-box',
583
+ }}>
584
+ {/* Play / pause — bare white triangle, no button chrome */}
585
+ <IconButton onClick={onPlayPause} tooltip={playing ? '暂停' : '播放'}>
586
+ {playing ? (
587
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
588
+ <path d="M3.33333 1.33398C2.59695 1.33398 2 1.93094 2 2.66732V13.334C2 14.0704 2.59695 14.6673 3.33333 14.6673H4.66667C5.40305 14.6673 6 14.0704 6 13.334V2.66732C6 1.93094 5.40305 1.33398 4.66667 1.33398H3.33333Z" fill="currentColor"/>
589
+ <path d="M11.3333 1.33398C10.597 1.33398 10 1.93094 10 2.66732V13.334C10 14.0704 10.597 14.6673 11.3333 14.6673H12.6667C13.403 14.6673 14 14.0704 14 13.334V2.66732C14 1.93094 13.403 1.33398 12.6667 1.33398H11.3333Z" fill="currentColor"/>
590
+ </svg>
591
+ ) : (
592
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
593
+ <path d="M14.0489 9.13127C14.873 8.60116 14.873 7.39754 14.0489 6.86743L4.74461 0.882617C3.84764 0.305661 2.66699 0.948902 2.66699 2.01454V13.9842C2.66699 15.0498 3.84764 15.693 4.74461 15.1161L14.0489 9.13127Z" fill="currentColor"/>
594
+ </svg>
595
+ )}
596
+ </IconButton>
597
+
598
+ {/* Current time */}
599
+ <div style={{
600
+ fontFamily: numFont,
601
+ fontSize: 14,
602
+ fontWeight: 400,
603
+ fontVariantNumeric: 'tabular-nums',
604
+ color: '#fff',
605
+ minWidth: 40,
606
+ textAlign: 'center'
607
+ }}>
608
+ {fmt(time)}
609
+ </div>
610
+
611
+ {/* Scrub track — white fill on translucent-white rail + draggable knob */}
612
+ <div
613
+ ref={trackRef}
614
+ onMouseMove={onTrackMove}
615
+ onMouseLeave={onTrackLeave}
616
+ onMouseDown={onTrackDown}
617
+ style={{
618
+ flex: 1,
619
+ height: 20,
620
+ position: 'relative',
621
+ cursor: 'pointer',
622
+ display: 'flex', alignItems: 'center',
623
+ }}
624
+ >
625
+ <div style={{
626
+ position: 'absolute',
627
+ left: 0, right: 0, height: 4,
628
+ background: 'rgba(255,255,255,0.6)',
629
+ borderRadius: 2,
630
+ }}/>
631
+ <div style={{
632
+ position: 'absolute',
633
+ left: 0, width: `${pct}%`, height: 4,
634
+ background: 'rgba(255,255,255,0.9)',
635
+ borderRadius: 2,
636
+ }}/>
637
+ {/* Progress knob — outer div is an enlarged transparent hit area for easier grabbing */}
638
+ <div
639
+ onMouseDown={onBallDown}
640
+ style={{
641
+ position: 'absolute',
642
+ left: `${pct}%`, top: '50%',
643
+ transform: 'translate(-50%, -50%)',
644
+ width: 20, height: 20,
645
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
646
+ cursor: dragging ? 'grabbing' : 'grab',
647
+ zIndex: 5,
648
+ }}
649
+ >
650
+ <div style={{
651
+ width: dragging ? 14 : 12,
652
+ height: dragging ? 14 : 12,
653
+ background: '#fff',
654
+ borderRadius: '50%',
655
+ border: '0.5px solid #D2D5D8',
656
+ boxShadow: '0 2px 6px rgba(0,0,0,0.35)',
657
+ transition: 'width 100ms, height 100ms',
658
+ }}/>
659
+ </div>
660
+ {trackHover && (
661
+ <div style={{
662
+ position: 'absolute',
663
+ left: trackHover.x,
664
+ bottom: '100%',
665
+ transform: 'translateX(-50%)',
666
+ marginBottom: 2,
667
+ pointerEvents: 'none',
668
+ zIndex: 10,
669
+ }}>
670
+ <TooltipBubble text={fmt(trackHover.t)} />
671
+ </div>
672
+ )}
673
+ </div>
674
+
675
+ {/* Duration — dimmed */}
676
+ <div style={{
677
+ fontFamily: numFont,
678
+ fontSize: 14,
679
+ fontWeight: 400,
680
+ fontVariantNumeric: 'tabular-nums',
681
+ color: 'rgba(255,255,255,0.6)',
682
+ minWidth: 40,
683
+ textAlign: 'center',
684
+ }}>
685
+ {fmt(duration)}
686
+ </div>
687
+ </div>
688
+ );
689
+ }
690
+
691
+
692
+ function IconButton({ children, onClick, tooltip }) {
693
+ const [hover, setHover] = React.useState(false);
694
+ return (
695
+ <button
696
+ onClick={onClick}
697
+ aria-label={tooltip}
698
+ onMouseEnter={() => setHover(true)}
699
+ onMouseLeave={() => setHover(false)}
700
+ style={{
701
+ position: 'relative',
702
+ width: 24, height: 24,
703
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
704
+ background: hover ? 'rgba(255,255,255,0.1)' : 'transparent',
705
+ border: 'none',
706
+ borderRadius: 6,
707
+ color: '#fff',
708
+ cursor: 'pointer',
709
+ padding: 0,
710
+ transition: 'background 120ms',
711
+ }}
712
+ >
713
+ {children}
714
+ {tooltip && (
715
+ <div style={{
716
+ position: 'absolute',
717
+ bottom: '100%',
718
+ left: '50%',
719
+ transform: 'translateX(-50%)',
720
+ marginBottom: 8,
721
+ pointerEvents: 'none',
722
+ opacity: hover ? 1 : 0,
723
+ transition: 'opacity 120ms',
724
+ zIndex: 10,
725
+ }}>
726
+ <TooltipBubble text={tooltip} />
727
+ </div>
728
+ )}
729
+ </button>
730
+ );
731
+ }
732
+
733
+ // ── Tooltip bubble ────────────────────────────────────────────────────────────
734
+ // Dark rounded bubble with a downward tail. Positioning is up to the caller.
735
+ function TooltipBubble({ text }) {
736
+ return (
737
+ <div style={{ position: 'relative', display: 'inline-block' }}>
738
+ <div style={{
739
+ background: '#1F2329',
740
+ color: '#fff',
741
+ fontSize: 12,
742
+ lineHeight: '16px',
743
+ padding: '6px 12px',
744
+ borderRadius: 6,
745
+ whiteSpace: 'nowrap',
746
+ fontFamily: '"PingFang SC", -apple-system, BlinkMacSystemFont, system-ui, sans-serif',
747
+ fontVariantNumeric: 'tabular-nums',
748
+ boxShadow: '0 4px 8px -8px rgba(0, 0, 0, 0.06), 0 6px 12px 0 rgba(0, 0, 0, 0.04), 0 8px 24px 8px rgba(0, 0, 0, 0.04)',
749
+ }}>
750
+ {text}
751
+ </div>
752
+ <div style={{
753
+ position: 'absolute',
754
+ top: '100%',
755
+ left: '50%',
756
+ transform: 'translate(-50%, -50%) rotate(45deg)',
757
+ width: 9, height: 9,
758
+ borderRadius: '0 0 3px 0',
759
+ background: '#1F2329',
760
+ }}/>
761
+ </div>
762
+ );
763
+ }
764
+
765
+
766
+ Object.assign(window, {
767
+ Easing, interpolate, animate, clamp,
768
+ TimelineContext, useTime, useTimeline,
769
+ Sprite, SpriteContext, useSprite,
770
+ TextSprite, ImageSprite, RectSprite,
771
+ Stage, PlaybackBar,
772
+ });
773
+