@prismer/runtime 2.0.7 → 2.2.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3631 -0
- package/README.md +34 -12
- package/apc/skills/FIELD-DICTIONARY.md +111 -0
- package/apc/skills/bug-reproduce/SKILL.md +150 -0
- package/apc/skills/bug-reproduce/skill.json +96 -0
- package/apc/skills/code-review/SKILL.md +198 -0
- package/apc/skills/code-review/skill.json +124 -0
- package/apc/skills/design-review/SKILL.md +122 -0
- package/apc/skills/design-review/skill.json +88 -0
- package/apc/skills/doc-sync/SKILL.md +168 -0
- package/apc/skills/doc-sync/skill.json +81 -0
- package/apc/skills/env-doctor/SKILL.md +194 -0
- package/apc/skills/env-doctor/skill.json +209 -0
- package/apc/skills/git-ops/SKILL.md +189 -0
- package/apc/skills/git-ops/skill.json +94 -0
- package/apc/skills/impact-trace/SKILL.md +168 -0
- package/apc/skills/impact-trace/skill.json +104 -0
- package/apc/skills/observability/SKILL.md +195 -0
- package/apc/skills/observability/skill.json +116 -0
- package/apc/skills/release-db-config-sync/SKILL.md +186 -0
- package/apc/skills/release-db-config-sync/skill.json +109 -0
- package/apc/skills/release-ota-promote/SKILL.md +195 -0
- package/apc/skills/release-ota-promote/skill.json +176 -0
- package/apc/skills/release-preflight/SKILL.md +174 -0
- package/apc/skills/release-preflight/skill.json +175 -0
- package/apc/skills/release-rollback/SKILL.md +214 -0
- package/apc/skills/release-rollback/skill.json +230 -0
- package/apc/skills/release-tag/SKILL.md +194 -0
- package/apc/skills/release-tag/skill.json +94 -0
- package/apc/skills/releasing-prod/SKILL.md +49 -0
- package/apc/skills/releasing-test/SKILL.md +135 -0
- package/apc/skills/sdk-release/SKILL.md +200 -0
- package/apc/skills/spec-intake/SKILL.md +169 -0
- package/apc/skills/spec-intake/skill.json +93 -0
- package/apc/skills/test-result-feedback/SKILL.md +239 -0
- package/apc/skills/test-result-feedback/skill.json +193 -0
- package/apc/skills/test-runner/SKILL.md +169 -0
- package/apc/skills/test-runner/skill.json +103 -0
- package/apc/skills/ui-align/SKILL.md +209 -0
- package/apc/skills/ui-align/skill.json +114 -0
- package/apc/skills/ui-canvas/SKILL.md +148 -0
- package/apc/skills/ui-canvas/skill.json +127 -0
- package/built-in-skills/agent-coordination/SKILL.md +257 -0
- package/built-in-skills/agent-meta/SKILL.md +53 -0
- package/built-in-skills/assets/SKILL.md +133 -0
- package/built-in-skills/browser-use/SKILL.md +93 -0
- package/built-in-skills/canvas-design/LICENSE.txt +202 -0
- package/built-in-skills/canvas-design/SKILL.md +157 -0
- package/built-in-skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Boldonse-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/DMMono-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/EricaOne-OFL.txt +94 -0
- package/built-in-skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/GeistMono-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Gloock-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Italiana-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Jura-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Lora-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/NationalPark-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Outfit-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/Tektur-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/WorkSans-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
- package/built-in-skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +93 -0
- package/built-in-skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
- package/built-in-skills/claim-agent-ownership/SKILL.md +255 -0
- package/built-in-skills/claude-api/LICENSE.txt +202 -0
- package/built-in-skills/claude-api/SKILL.md +325 -0
- package/built-in-skills/claude-api/csharp/claude-api.md +402 -0
- package/built-in-skills/claude-api/curl/examples.md +216 -0
- package/built-in-skills/claude-api/curl/managed-agents.md +336 -0
- package/built-in-skills/claude-api/go/claude-api.md +421 -0
- package/built-in-skills/claude-api/go/managed-agents/README.md +561 -0
- package/built-in-skills/claude-api/java/claude-api.md +432 -0
- package/built-in-skills/claude-api/java/managed-agents/README.md +442 -0
- package/built-in-skills/claude-api/php/claude-api.md +375 -0
- package/built-in-skills/claude-api/php/managed-agents/README.md +435 -0
- package/built-in-skills/claude-api/python/claude-api/README.md +420 -0
- package/built-in-skills/claude-api/python/claude-api/batches.md +185 -0
- package/built-in-skills/claude-api/python/claude-api/files-api.md +165 -0
- package/built-in-skills/claude-api/python/claude-api/streaming.md +162 -0
- package/built-in-skills/claude-api/python/claude-api/tool-use.md +590 -0
- package/built-in-skills/claude-api/python/managed-agents/README.md +332 -0
- package/built-in-skills/claude-api/ruby/claude-api.md +113 -0
- package/built-in-skills/claude-api/ruby/managed-agents/README.md +389 -0
- package/built-in-skills/claude-api/shared/agent-design.md +101 -0
- package/built-in-skills/claude-api/shared/error-codes.md +213 -0
- package/built-in-skills/claude-api/shared/live-sources.md +135 -0
- package/built-in-skills/claude-api/shared/managed-agents-api-reference.md +378 -0
- package/built-in-skills/claude-api/shared/managed-agents-client-patterns.md +209 -0
- package/built-in-skills/claude-api/shared/managed-agents-core.md +238 -0
- package/built-in-skills/claude-api/shared/managed-agents-environments.md +215 -0
- package/built-in-skills/claude-api/shared/managed-agents-events.md +195 -0
- package/built-in-skills/claude-api/shared/managed-agents-memory.md +197 -0
- package/built-in-skills/claude-api/shared/managed-agents-multiagent.md +99 -0
- package/built-in-skills/claude-api/shared/managed-agents-onboarding.md +114 -0
- package/built-in-skills/claude-api/shared/managed-agents-outcomes.md +106 -0
- package/built-in-skills/claude-api/shared/managed-agents-overview.md +68 -0
- package/built-in-skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +173 -0
- package/built-in-skills/claude-api/shared/managed-agents-tools.md +321 -0
- package/built-in-skills/claude-api/shared/managed-agents-webhooks.md +110 -0
- package/built-in-skills/claude-api/shared/model-migration.md +779 -0
- package/built-in-skills/claude-api/shared/models.md +121 -0
- package/built-in-skills/claude-api/shared/prompt-caching.md +171 -0
- package/built-in-skills/claude-api/shared/tool-use-concepts.md +327 -0
- package/built-in-skills/claude-api/typescript/claude-api/README.md +333 -0
- package/built-in-skills/claude-api/typescript/claude-api/batches.md +106 -0
- package/built-in-skills/claude-api/typescript/claude-api/files-api.md +98 -0
- package/built-in-skills/claude-api/typescript/claude-api/streaming.md +178 -0
- package/built-in-skills/claude-api/typescript/claude-api/tool-use.md +527 -0
- package/built-in-skills/claude-api/typescript/managed-agents/README.md +359 -0
- package/built-in-skills/codebase-design/DEEPENING.md +37 -0
- package/built-in-skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/built-in-skills/codebase-design/LICENSE +21 -0
- package/built-in-skills/codebase-design/SKILL.md +116 -0
- package/built-in-skills/conversation-compaction/SKILL.md +114 -0
- package/built-in-skills/council-creator/SKILL.md +426 -0
- package/built-in-skills/diagnosing-bugs/LICENSE +21 -0
- package/built-in-skills/diagnosing-bugs/SKILL.md +136 -0
- package/built-in-skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/built-in-skills/doc-coauthoring/SKILL.md +376 -0
- package/built-in-skills/document-generation/SKILL.md +105 -0
- package/built-in-skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/built-in-skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/built-in-skills/domain-modeling/LICENSE +21 -0
- package/built-in-skills/domain-modeling/SKILL.md +76 -0
- package/built-in-skills/frontend-design/LICENSE.txt +177 -0
- package/built-in-skills/frontend-design/SKILL.md +43 -0
- package/built-in-skills/human-approval/SKILL.md +129 -0
- package/built-in-skills/image-generate/SKILL.md +128 -0
- package/built-in-skills/image-generate/scripts/generate-and-deliver.mjs +289 -0
- package/built-in-skills/ingest/SKILL.md +73 -0
- package/built-in-skills/internal-comms/LICENSE.txt +202 -0
- package/built-in-skills/internal-comms/SKILL.md +33 -0
- package/built-in-skills/internal-comms/examples/3p-updates.md +47 -0
- package/built-in-skills/internal-comms/examples/company-newsletter.md +65 -0
- package/built-in-skills/internal-comms/examples/faq-answers.md +30 -0
- package/built-in-skills/internal-comms/examples/general-comms.md +16 -0
- package/built-in-skills/liteparse/SKILL.md +176 -0
- package/built-in-skills/mcp-builder/LICENSE.txt +202 -0
- package/built-in-skills/mcp-builder/SKILL.md +237 -0
- package/built-in-skills/mcp-builder/reference/evaluation.md +602 -0
- package/built-in-skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/built-in-skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/built-in-skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/built-in-skills/mcp-builder/scripts/connections.py +151 -0
- package/built-in-skills/mcp-builder/scripts/evaluation.py +373 -0
- package/built-in-skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/built-in-skills/mcp-builder/scripts/requirements.txt +2 -0
- package/built-in-skills/memory/SKILL.md +471 -0
- package/built-in-skills/memory-dream/SKILL.md +339 -0
- package/built-in-skills/office-artifacts/SKILL.md +211 -0
- package/built-in-skills/okr/SKILL.md +154 -0
- package/built-in-skills/persona/SKILL.md +81 -0
- package/built-in-skills/persona-generator/SKILL.md +296 -0
- package/built-in-skills/pkf-svg/SKILL.md +253 -0
- package/built-in-skills/pkf-writing/SKILL.md +236 -0
- package/built-in-skills/prismer-im-collab/SKILL.md +168 -0
- package/built-in-skills/proactivity/SKILL.md +84 -0
- package/built-in-skills/remotion/SKILL.md +431 -0
- package/built-in-skills/role-builder/SKILL.md +203 -0
- package/built-in-skills/role-builder/scripts/author-role.mjs +334 -0
- package/built-in-skills/role-builder/scripts/ingest-role.mjs +223 -0
- package/built-in-skills/role-builder/scripts/instantiate-and-run.mjs +290 -0
- package/built-in-skills/role-builder/scripts/operation-harness.mjs +267 -0
- package/built-in-skills/skill-authoring/SKILL.md +134 -0
- package/built-in-skills/skill-authoring/skill.json +74 -0
- package/built-in-skills/skill-builder/SKILL.md +171 -0
- package/built-in-skills/skill-builder/scripts/ingest.mjs +265 -0
- package/built-in-skills/skill-creator/LICENSE.txt +202 -0
- package/built-in-skills/skill-creator/SKILL.md +227 -0
- package/built-in-skills/skill-creator/agents/analyzer.md +274 -0
- package/built-in-skills/skill-creator/agents/comparator.md +202 -0
- package/built-in-skills/skill-creator/agents/grader.md +223 -0
- package/built-in-skills/skill-creator/assets/eval_review.html +146 -0
- package/built-in-skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/built-in-skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/built-in-skills/skill-creator/references/external-library-import.md +110 -0
- package/built-in-skills/skill-creator/references/schemas.md +430 -0
- package/built-in-skills/skill-creator/scripts/__init__.py +0 -0
- package/built-in-skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/built-in-skills/skill-creator/scripts/generate_report.py +326 -0
- package/built-in-skills/skill-creator/scripts/import-library.mjs +475 -0
- package/built-in-skills/skill-creator/scripts/improve_description.py +247 -0
- package/built-in-skills/skill-creator/scripts/package_skill.py +136 -0
- package/built-in-skills/skill-creator/scripts/quick_validate.py +103 -0
- package/built-in-skills/skill-creator/scripts/run_eval.py +310 -0
- package/built-in-skills/skill-creator/scripts/run_loop.py +328 -0
- package/built-in-skills/skill-creator/scripts/utils.py +47 -0
- package/built-in-skills/slack-gif-creator/LICENSE.txt +202 -0
- package/built-in-skills/slack-gif-creator/SKILL.md +291 -0
- package/built-in-skills/slack-gif-creator/core/easing.py +234 -0
- package/built-in-skills/slack-gif-creator/core/frame_composer.py +176 -0
- package/built-in-skills/slack-gif-creator/core/gif_builder.py +269 -0
- package/built-in-skills/slack-gif-creator/core/validators.py +136 -0
- package/built-in-skills/slack-gif-creator/requirements.txt +4 -0
- package/built-in-skills/tasks/SKILL.md +413 -0
- package/built-in-skills/tdd/LICENSE +21 -0
- package/built-in-skills/tdd/SKILL.md +110 -0
- package/built-in-skills/tdd/mocking.md +59 -0
- package/built-in-skills/tdd/refactoring.md +10 -0
- package/built-in-skills/tdd/tests.md +61 -0
- package/built-in-skills/team/SKILL.md +77 -0
- package/built-in-skills/web-artifacts-builder/LICENSE.txt +202 -0
- package/built-in-skills/web-artifacts-builder/SKILL.md +105 -0
- package/built-in-skills/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/built-in-skills/web-artifacts-builder/scripts/init-artifact.sh +334 -0
- package/built-in-skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/built-in-skills/webapp-testing/LICENSE.txt +202 -0
- package/built-in-skills/webapp-testing/SKILL.md +97 -0
- package/built-in-skills/webapp-testing/examples/console_logging.py +35 -0
- package/built-in-skills/webapp-testing/examples/element_discovery.py +40 -0
- package/built-in-skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/built-in-skills/webapp-testing/scripts/with_server.py +106 -0
- package/built-in-skills/wechat-pay/SKILL.md +59 -0
- package/dist/cli.cjs +72577 -16101
- package/dist/cli.js +72752 -16232
- package/dist/index.cjs +72632 -16024
- package/dist/index.d.cts +4956 -640
- package/dist/index.d.ts +4956 -640
- package/dist/index.js +72564 -15963
- package/package.json +39 -6
- package/plugins/memory/prismer/__init__.py +1211 -0
- package/plugins/memory/prismer/plugin.yaml +8 -0
- package/plugins/memory/prismer/tool-schemas.generated.json +249 -0
- package/plugins/tools/prismer-recall/__init__.py +282 -0
- package/plugins/tools/prismer-recall/plugin.yaml +15 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Easing Functions - Timing functions for smooth animations.
|
|
4
|
+
|
|
5
|
+
Provides various easing functions for natural motion and timing.
|
|
6
|
+
All functions take a value t (0.0 to 1.0) and return eased value (0.0 to 1.0).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def linear(t: float) -> float:
|
|
13
|
+
"""Linear interpolation (no easing)."""
|
|
14
|
+
return t
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ease_in_quad(t: float) -> float:
|
|
18
|
+
"""Quadratic ease-in (slow start, accelerating)."""
|
|
19
|
+
return t * t
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def ease_out_quad(t: float) -> float:
|
|
23
|
+
"""Quadratic ease-out (fast start, decelerating)."""
|
|
24
|
+
return t * (2 - t)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def ease_in_out_quad(t: float) -> float:
|
|
28
|
+
"""Quadratic ease-in-out (slow start and end)."""
|
|
29
|
+
if t < 0.5:
|
|
30
|
+
return 2 * t * t
|
|
31
|
+
return -1 + (4 - 2 * t) * t
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def ease_in_cubic(t: float) -> float:
|
|
35
|
+
"""Cubic ease-in (slow start)."""
|
|
36
|
+
return t * t * t
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ease_out_cubic(t: float) -> float:
|
|
40
|
+
"""Cubic ease-out (fast start)."""
|
|
41
|
+
return (t - 1) * (t - 1) * (t - 1) + 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def ease_in_out_cubic(t: float) -> float:
|
|
45
|
+
"""Cubic ease-in-out."""
|
|
46
|
+
if t < 0.5:
|
|
47
|
+
return 4 * t * t * t
|
|
48
|
+
return (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def ease_in_bounce(t: float) -> float:
|
|
52
|
+
"""Bounce ease-in (bouncy start)."""
|
|
53
|
+
return 1 - ease_out_bounce(1 - t)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def ease_out_bounce(t: float) -> float:
|
|
57
|
+
"""Bounce ease-out (bouncy end)."""
|
|
58
|
+
if t < 1 / 2.75:
|
|
59
|
+
return 7.5625 * t * t
|
|
60
|
+
elif t < 2 / 2.75:
|
|
61
|
+
t -= 1.5 / 2.75
|
|
62
|
+
return 7.5625 * t * t + 0.75
|
|
63
|
+
elif t < 2.5 / 2.75:
|
|
64
|
+
t -= 2.25 / 2.75
|
|
65
|
+
return 7.5625 * t * t + 0.9375
|
|
66
|
+
else:
|
|
67
|
+
t -= 2.625 / 2.75
|
|
68
|
+
return 7.5625 * t * t + 0.984375
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def ease_in_out_bounce(t: float) -> float:
|
|
72
|
+
"""Bounce ease-in-out."""
|
|
73
|
+
if t < 0.5:
|
|
74
|
+
return ease_in_bounce(t * 2) * 0.5
|
|
75
|
+
return ease_out_bounce(t * 2 - 1) * 0.5 + 0.5
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def ease_in_elastic(t: float) -> float:
|
|
79
|
+
"""Elastic ease-in (spring effect)."""
|
|
80
|
+
if t == 0 or t == 1:
|
|
81
|
+
return t
|
|
82
|
+
return -math.pow(2, 10 * (t - 1)) * math.sin((t - 1.1) * 5 * math.pi)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def ease_out_elastic(t: float) -> float:
|
|
86
|
+
"""Elastic ease-out (spring effect)."""
|
|
87
|
+
if t == 0 or t == 1:
|
|
88
|
+
return t
|
|
89
|
+
return math.pow(2, -10 * t) * math.sin((t - 0.1) * 5 * math.pi) + 1
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def ease_in_out_elastic(t: float) -> float:
|
|
93
|
+
"""Elastic ease-in-out."""
|
|
94
|
+
if t == 0 or t == 1:
|
|
95
|
+
return t
|
|
96
|
+
t = t * 2 - 1
|
|
97
|
+
if t < 0:
|
|
98
|
+
return -0.5 * math.pow(2, 10 * t) * math.sin((t - 0.1) * 5 * math.pi)
|
|
99
|
+
return math.pow(2, -10 * t) * math.sin((t - 0.1) * 5 * math.pi) * 0.5 + 1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Convenience mapping
|
|
103
|
+
EASING_FUNCTIONS = {
|
|
104
|
+
"linear": linear,
|
|
105
|
+
"ease_in": ease_in_quad,
|
|
106
|
+
"ease_out": ease_out_quad,
|
|
107
|
+
"ease_in_out": ease_in_out_quad,
|
|
108
|
+
"bounce_in": ease_in_bounce,
|
|
109
|
+
"bounce_out": ease_out_bounce,
|
|
110
|
+
"bounce": ease_in_out_bounce,
|
|
111
|
+
"elastic_in": ease_in_elastic,
|
|
112
|
+
"elastic_out": ease_out_elastic,
|
|
113
|
+
"elastic": ease_in_out_elastic,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_easing(name: str = "linear"):
|
|
118
|
+
"""Get easing function by name."""
|
|
119
|
+
return EASING_FUNCTIONS.get(name, linear)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def interpolate(start: float, end: float, t: float, easing: str = "linear") -> float:
|
|
123
|
+
"""
|
|
124
|
+
Interpolate between two values with easing.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
start: Start value
|
|
128
|
+
end: End value
|
|
129
|
+
t: Progress from 0.0 to 1.0
|
|
130
|
+
easing: Name of easing function
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Interpolated value
|
|
134
|
+
"""
|
|
135
|
+
ease_func = get_easing(easing)
|
|
136
|
+
eased_t = ease_func(t)
|
|
137
|
+
return start + (end - start) * eased_t
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def ease_back_in(t: float) -> float:
|
|
141
|
+
"""Back ease-in (slight overshoot backward before forward motion)."""
|
|
142
|
+
c1 = 1.70158
|
|
143
|
+
c3 = c1 + 1
|
|
144
|
+
return c3 * t * t * t - c1 * t * t
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def ease_back_out(t: float) -> float:
|
|
148
|
+
"""Back ease-out (overshoot forward then settle back)."""
|
|
149
|
+
c1 = 1.70158
|
|
150
|
+
c3 = c1 + 1
|
|
151
|
+
return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def ease_back_in_out(t: float) -> float:
|
|
155
|
+
"""Back ease-in-out (overshoot at both ends)."""
|
|
156
|
+
c1 = 1.70158
|
|
157
|
+
c2 = c1 * 1.525
|
|
158
|
+
if t < 0.5:
|
|
159
|
+
return (pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
|
|
160
|
+
return (pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def apply_squash_stretch(
|
|
164
|
+
base_scale: tuple[float, float], intensity: float, direction: str = "vertical"
|
|
165
|
+
) -> tuple[float, float]:
|
|
166
|
+
"""
|
|
167
|
+
Calculate squash and stretch scales for more dynamic animation.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
base_scale: (width_scale, height_scale) base scales
|
|
171
|
+
intensity: Squash/stretch intensity (0.0-1.0)
|
|
172
|
+
direction: 'vertical', 'horizontal', or 'both'
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
(width_scale, height_scale) with squash/stretch applied
|
|
176
|
+
"""
|
|
177
|
+
width_scale, height_scale = base_scale
|
|
178
|
+
|
|
179
|
+
if direction == "vertical":
|
|
180
|
+
# Compress vertically, expand horizontally (preserve volume)
|
|
181
|
+
height_scale *= 1 - intensity * 0.5
|
|
182
|
+
width_scale *= 1 + intensity * 0.5
|
|
183
|
+
elif direction == "horizontal":
|
|
184
|
+
# Compress horizontally, expand vertically
|
|
185
|
+
width_scale *= 1 - intensity * 0.5
|
|
186
|
+
height_scale *= 1 + intensity * 0.5
|
|
187
|
+
elif direction == "both":
|
|
188
|
+
# General squash (both dimensions)
|
|
189
|
+
width_scale *= 1 - intensity * 0.3
|
|
190
|
+
height_scale *= 1 - intensity * 0.3
|
|
191
|
+
|
|
192
|
+
return (width_scale, height_scale)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def calculate_arc_motion(
|
|
196
|
+
start: tuple[float, float], end: tuple[float, float], height: float, t: float
|
|
197
|
+
) -> tuple[float, float]:
|
|
198
|
+
"""
|
|
199
|
+
Calculate position along a parabolic arc (natural motion path).
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
start: (x, y) starting position
|
|
203
|
+
end: (x, y) ending position
|
|
204
|
+
height: Arc height at midpoint (positive = upward)
|
|
205
|
+
t: Progress (0.0-1.0)
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
(x, y) position along arc
|
|
209
|
+
"""
|
|
210
|
+
x1, y1 = start
|
|
211
|
+
x2, y2 = end
|
|
212
|
+
|
|
213
|
+
# Linear interpolation for x
|
|
214
|
+
x = x1 + (x2 - x1) * t
|
|
215
|
+
|
|
216
|
+
# Parabolic interpolation for y
|
|
217
|
+
# y = start + progress * (end - start) + arc_offset
|
|
218
|
+
# Arc offset peaks at t=0.5
|
|
219
|
+
arc_offset = 4 * height * t * (1 - t)
|
|
220
|
+
y = y1 + (y2 - y1) * t - arc_offset
|
|
221
|
+
|
|
222
|
+
return (x, y)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# Add new easing functions to the convenience mapping
|
|
226
|
+
EASING_FUNCTIONS.update(
|
|
227
|
+
{
|
|
228
|
+
"back_in": ease_back_in,
|
|
229
|
+
"back_out": ease_back_out,
|
|
230
|
+
"back_in_out": ease_back_in_out,
|
|
231
|
+
"anticipate": ease_back_in, # Alias
|
|
232
|
+
"overshoot": ease_back_out, # Alias
|
|
233
|
+
}
|
|
234
|
+
)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Frame Composer - Utilities for composing visual elements into frames.
|
|
4
|
+
|
|
5
|
+
Provides functions for drawing shapes, text, emojis, and compositing elements
|
|
6
|
+
together to create animation frames.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_blank_frame(
|
|
16
|
+
width: int, height: int, color: tuple[int, int, int] = (255, 255, 255)
|
|
17
|
+
) -> Image.Image:
|
|
18
|
+
"""
|
|
19
|
+
Create a blank frame with solid color background.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
width: Frame width
|
|
23
|
+
height: Frame height
|
|
24
|
+
color: RGB color tuple (default: white)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
PIL Image
|
|
28
|
+
"""
|
|
29
|
+
return Image.new("RGB", (width, height), color)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def draw_circle(
|
|
33
|
+
frame: Image.Image,
|
|
34
|
+
center: tuple[int, int],
|
|
35
|
+
radius: int,
|
|
36
|
+
fill_color: Optional[tuple[int, int, int]] = None,
|
|
37
|
+
outline_color: Optional[tuple[int, int, int]] = None,
|
|
38
|
+
outline_width: int = 1,
|
|
39
|
+
) -> Image.Image:
|
|
40
|
+
"""
|
|
41
|
+
Draw a circle on a frame.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
frame: PIL Image to draw on
|
|
45
|
+
center: (x, y) center position
|
|
46
|
+
radius: Circle radius
|
|
47
|
+
fill_color: RGB fill color (None for no fill)
|
|
48
|
+
outline_color: RGB outline color (None for no outline)
|
|
49
|
+
outline_width: Outline width in pixels
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Modified frame
|
|
53
|
+
"""
|
|
54
|
+
draw = ImageDraw.Draw(frame)
|
|
55
|
+
x, y = center
|
|
56
|
+
bbox = [x - radius, y - radius, x + radius, y + radius]
|
|
57
|
+
draw.ellipse(bbox, fill=fill_color, outline=outline_color, width=outline_width)
|
|
58
|
+
return frame
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def draw_text(
|
|
62
|
+
frame: Image.Image,
|
|
63
|
+
text: str,
|
|
64
|
+
position: tuple[int, int],
|
|
65
|
+
color: tuple[int, int, int] = (0, 0, 0),
|
|
66
|
+
centered: bool = False,
|
|
67
|
+
) -> Image.Image:
|
|
68
|
+
"""
|
|
69
|
+
Draw text on a frame.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
frame: PIL Image to draw on
|
|
73
|
+
text: Text to draw
|
|
74
|
+
position: (x, y) position (top-left unless centered=True)
|
|
75
|
+
color: RGB text color
|
|
76
|
+
centered: If True, center text at position
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Modified frame
|
|
80
|
+
"""
|
|
81
|
+
draw = ImageDraw.Draw(frame)
|
|
82
|
+
|
|
83
|
+
# Uses Pillow's default font.
|
|
84
|
+
# If the font should be changed for the emoji, add additional logic here.
|
|
85
|
+
font = ImageFont.load_default()
|
|
86
|
+
|
|
87
|
+
if centered:
|
|
88
|
+
bbox = draw.textbbox((0, 0), text, font=font)
|
|
89
|
+
text_width = bbox[2] - bbox[0]
|
|
90
|
+
text_height = bbox[3] - bbox[1]
|
|
91
|
+
x = position[0] - text_width // 2
|
|
92
|
+
y = position[1] - text_height // 2
|
|
93
|
+
position = (x, y)
|
|
94
|
+
|
|
95
|
+
draw.text(position, text, fill=color, font=font)
|
|
96
|
+
return frame
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def create_gradient_background(
|
|
100
|
+
width: int,
|
|
101
|
+
height: int,
|
|
102
|
+
top_color: tuple[int, int, int],
|
|
103
|
+
bottom_color: tuple[int, int, int],
|
|
104
|
+
) -> Image.Image:
|
|
105
|
+
"""
|
|
106
|
+
Create a vertical gradient background.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
width: Frame width
|
|
110
|
+
height: Frame height
|
|
111
|
+
top_color: RGB color at top
|
|
112
|
+
bottom_color: RGB color at bottom
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
PIL Image with gradient
|
|
116
|
+
"""
|
|
117
|
+
frame = Image.new("RGB", (width, height))
|
|
118
|
+
draw = ImageDraw.Draw(frame)
|
|
119
|
+
|
|
120
|
+
# Calculate color step for each row
|
|
121
|
+
r1, g1, b1 = top_color
|
|
122
|
+
r2, g2, b2 = bottom_color
|
|
123
|
+
|
|
124
|
+
for y in range(height):
|
|
125
|
+
# Interpolate color
|
|
126
|
+
ratio = y / height
|
|
127
|
+
r = int(r1 * (1 - ratio) + r2 * ratio)
|
|
128
|
+
g = int(g1 * (1 - ratio) + g2 * ratio)
|
|
129
|
+
b = int(b1 * (1 - ratio) + b2 * ratio)
|
|
130
|
+
|
|
131
|
+
# Draw horizontal line
|
|
132
|
+
draw.line([(0, y), (width, y)], fill=(r, g, b))
|
|
133
|
+
|
|
134
|
+
return frame
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def draw_star(
|
|
138
|
+
frame: Image.Image,
|
|
139
|
+
center: tuple[int, int],
|
|
140
|
+
size: int,
|
|
141
|
+
fill_color: tuple[int, int, int],
|
|
142
|
+
outline_color: Optional[tuple[int, int, int]] = None,
|
|
143
|
+
outline_width: int = 1,
|
|
144
|
+
) -> Image.Image:
|
|
145
|
+
"""
|
|
146
|
+
Draw a 5-pointed star.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
frame: PIL Image to draw on
|
|
150
|
+
center: (x, y) center position
|
|
151
|
+
size: Star size (outer radius)
|
|
152
|
+
fill_color: RGB fill color
|
|
153
|
+
outline_color: RGB outline color (None for no outline)
|
|
154
|
+
outline_width: Outline width
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Modified frame
|
|
158
|
+
"""
|
|
159
|
+
import math
|
|
160
|
+
|
|
161
|
+
draw = ImageDraw.Draw(frame)
|
|
162
|
+
x, y = center
|
|
163
|
+
|
|
164
|
+
# Calculate star points
|
|
165
|
+
points = []
|
|
166
|
+
for i in range(10):
|
|
167
|
+
angle = (i * 36 - 90) * math.pi / 180 # 36 degrees per point, start at top
|
|
168
|
+
radius = size if i % 2 == 0 else size * 0.4 # Alternate between outer and inner
|
|
169
|
+
px = x + radius * math.cos(angle)
|
|
170
|
+
py = y + radius * math.sin(angle)
|
|
171
|
+
points.append((px, py))
|
|
172
|
+
|
|
173
|
+
# Draw star
|
|
174
|
+
draw.polygon(points, fill=fill_color, outline=outline_color, width=outline_width)
|
|
175
|
+
|
|
176
|
+
return frame
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
GIF Builder - Core module for assembling frames into GIFs optimized for Slack.
|
|
4
|
+
|
|
5
|
+
This module provides the main interface for creating GIFs from programmatically
|
|
6
|
+
generated frames, with automatic optimization for Slack's requirements.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import imageio.v3 as imageio
|
|
13
|
+
import numpy as np
|
|
14
|
+
from PIL import Image
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GIFBuilder:
|
|
18
|
+
"""Builder for creating optimized GIFs from frames."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, width: int = 480, height: int = 480, fps: int = 15):
|
|
21
|
+
"""
|
|
22
|
+
Initialize GIF builder.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
width: Frame width in pixels
|
|
26
|
+
height: Frame height in pixels
|
|
27
|
+
fps: Frames per second
|
|
28
|
+
"""
|
|
29
|
+
self.width = width
|
|
30
|
+
self.height = height
|
|
31
|
+
self.fps = fps
|
|
32
|
+
self.frames: list[np.ndarray] = []
|
|
33
|
+
|
|
34
|
+
def add_frame(self, frame: np.ndarray | Image.Image):
|
|
35
|
+
"""
|
|
36
|
+
Add a frame to the GIF.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
frame: Frame as numpy array or PIL Image (will be converted to RGB)
|
|
40
|
+
"""
|
|
41
|
+
if isinstance(frame, Image.Image):
|
|
42
|
+
frame = np.array(frame.convert("RGB"))
|
|
43
|
+
|
|
44
|
+
# Ensure frame is correct size
|
|
45
|
+
if frame.shape[:2] != (self.height, self.width):
|
|
46
|
+
pil_frame = Image.fromarray(frame)
|
|
47
|
+
pil_frame = pil_frame.resize(
|
|
48
|
+
(self.width, self.height), Image.Resampling.LANCZOS
|
|
49
|
+
)
|
|
50
|
+
frame = np.array(pil_frame)
|
|
51
|
+
|
|
52
|
+
self.frames.append(frame)
|
|
53
|
+
|
|
54
|
+
def add_frames(self, frames: list[np.ndarray | Image.Image]):
|
|
55
|
+
"""Add multiple frames at once."""
|
|
56
|
+
for frame in frames:
|
|
57
|
+
self.add_frame(frame)
|
|
58
|
+
|
|
59
|
+
def optimize_colors(
|
|
60
|
+
self, num_colors: int = 128, use_global_palette: bool = True
|
|
61
|
+
) -> list[np.ndarray]:
|
|
62
|
+
"""
|
|
63
|
+
Reduce colors in all frames using quantization.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
num_colors: Target number of colors (8-256)
|
|
67
|
+
use_global_palette: Use a single palette for all frames (better compression)
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List of color-optimized frames
|
|
71
|
+
"""
|
|
72
|
+
optimized = []
|
|
73
|
+
|
|
74
|
+
if use_global_palette and len(self.frames) > 1:
|
|
75
|
+
# Create a global palette from all frames
|
|
76
|
+
# Sample frames to build palette
|
|
77
|
+
sample_size = min(5, len(self.frames))
|
|
78
|
+
sample_indices = [
|
|
79
|
+
int(i * len(self.frames) / sample_size) for i in range(sample_size)
|
|
80
|
+
]
|
|
81
|
+
sample_frames = [self.frames[i] for i in sample_indices]
|
|
82
|
+
|
|
83
|
+
# Combine sample frames into a single image for palette generation
|
|
84
|
+
# Flatten each frame to get all pixels, then stack them
|
|
85
|
+
all_pixels = np.vstack(
|
|
86
|
+
[f.reshape(-1, 3) for f in sample_frames]
|
|
87
|
+
) # (total_pixels, 3)
|
|
88
|
+
|
|
89
|
+
# Create a properly-shaped RGB image from the pixel data
|
|
90
|
+
# We'll make a roughly square image from all the pixels
|
|
91
|
+
total_pixels = len(all_pixels)
|
|
92
|
+
width = min(512, int(np.sqrt(total_pixels))) # Reasonable width, max 512
|
|
93
|
+
height = (total_pixels + width - 1) // width # Ceiling division
|
|
94
|
+
|
|
95
|
+
# Pad if necessary to fill the rectangle
|
|
96
|
+
pixels_needed = width * height
|
|
97
|
+
if pixels_needed > total_pixels:
|
|
98
|
+
padding = np.zeros((pixels_needed - total_pixels, 3), dtype=np.uint8)
|
|
99
|
+
all_pixels = np.vstack([all_pixels, padding])
|
|
100
|
+
|
|
101
|
+
# Reshape to proper RGB image format (H, W, 3)
|
|
102
|
+
img_array = (
|
|
103
|
+
all_pixels[:pixels_needed].reshape(height, width, 3).astype(np.uint8)
|
|
104
|
+
)
|
|
105
|
+
combined_img = Image.fromarray(img_array, mode="RGB")
|
|
106
|
+
|
|
107
|
+
# Generate global palette
|
|
108
|
+
global_palette = combined_img.quantize(colors=num_colors, method=2)
|
|
109
|
+
|
|
110
|
+
# Apply global palette to all frames
|
|
111
|
+
for frame in self.frames:
|
|
112
|
+
pil_frame = Image.fromarray(frame)
|
|
113
|
+
quantized = pil_frame.quantize(palette=global_palette, dither=1)
|
|
114
|
+
optimized.append(np.array(quantized.convert("RGB")))
|
|
115
|
+
else:
|
|
116
|
+
# Use per-frame quantization
|
|
117
|
+
for frame in self.frames:
|
|
118
|
+
pil_frame = Image.fromarray(frame)
|
|
119
|
+
quantized = pil_frame.quantize(colors=num_colors, method=2, dither=1)
|
|
120
|
+
optimized.append(np.array(quantized.convert("RGB")))
|
|
121
|
+
|
|
122
|
+
return optimized
|
|
123
|
+
|
|
124
|
+
def deduplicate_frames(self, threshold: float = 0.9995) -> int:
|
|
125
|
+
"""
|
|
126
|
+
Remove duplicate or near-duplicate consecutive frames.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
threshold: Similarity threshold (0.0-1.0). Higher = more strict (0.9995 = nearly identical).
|
|
130
|
+
Use 0.9995+ to preserve subtle animations, 0.98 for aggressive removal.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Number of frames removed
|
|
134
|
+
"""
|
|
135
|
+
if len(self.frames) < 2:
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
deduplicated = [self.frames[0]]
|
|
139
|
+
removed_count = 0
|
|
140
|
+
|
|
141
|
+
for i in range(1, len(self.frames)):
|
|
142
|
+
# Compare with previous frame
|
|
143
|
+
prev_frame = np.array(deduplicated[-1], dtype=np.float32)
|
|
144
|
+
curr_frame = np.array(self.frames[i], dtype=np.float32)
|
|
145
|
+
|
|
146
|
+
# Calculate similarity (normalized)
|
|
147
|
+
diff = np.abs(prev_frame - curr_frame)
|
|
148
|
+
similarity = 1.0 - (np.mean(diff) / 255.0)
|
|
149
|
+
|
|
150
|
+
# Keep frame if sufficiently different
|
|
151
|
+
# High threshold (0.9995+) means only remove nearly identical frames
|
|
152
|
+
if similarity < threshold:
|
|
153
|
+
deduplicated.append(self.frames[i])
|
|
154
|
+
else:
|
|
155
|
+
removed_count += 1
|
|
156
|
+
|
|
157
|
+
self.frames = deduplicated
|
|
158
|
+
return removed_count
|
|
159
|
+
|
|
160
|
+
def save(
|
|
161
|
+
self,
|
|
162
|
+
output_path: str | Path,
|
|
163
|
+
num_colors: int = 128,
|
|
164
|
+
optimize_for_emoji: bool = False,
|
|
165
|
+
remove_duplicates: bool = False,
|
|
166
|
+
) -> dict:
|
|
167
|
+
"""
|
|
168
|
+
Save frames as optimized GIF for Slack.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
output_path: Where to save the GIF
|
|
172
|
+
num_colors: Number of colors to use (fewer = smaller file)
|
|
173
|
+
optimize_for_emoji: If True, optimize for emoji size (128x128, fewer colors)
|
|
174
|
+
remove_duplicates: If True, remove duplicate consecutive frames (opt-in)
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Dictionary with file info (path, size, dimensions, frame_count)
|
|
178
|
+
"""
|
|
179
|
+
if not self.frames:
|
|
180
|
+
raise ValueError("No frames to save. Add frames with add_frame() first.")
|
|
181
|
+
|
|
182
|
+
output_path = Path(output_path)
|
|
183
|
+
|
|
184
|
+
# Remove duplicate frames to reduce file size
|
|
185
|
+
if remove_duplicates:
|
|
186
|
+
removed = self.deduplicate_frames(threshold=0.9995)
|
|
187
|
+
if removed > 0:
|
|
188
|
+
print(
|
|
189
|
+
f" Removed {removed} nearly identical frames (preserved subtle animations)"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Optimize for emoji if requested
|
|
193
|
+
if optimize_for_emoji:
|
|
194
|
+
if self.width > 128 or self.height > 128:
|
|
195
|
+
print(
|
|
196
|
+
f" Resizing from {self.width}x{self.height} to 128x128 for emoji"
|
|
197
|
+
)
|
|
198
|
+
self.width = 128
|
|
199
|
+
self.height = 128
|
|
200
|
+
# Resize all frames
|
|
201
|
+
resized_frames = []
|
|
202
|
+
for frame in self.frames:
|
|
203
|
+
pil_frame = Image.fromarray(frame)
|
|
204
|
+
pil_frame = pil_frame.resize((128, 128), Image.Resampling.LANCZOS)
|
|
205
|
+
resized_frames.append(np.array(pil_frame))
|
|
206
|
+
self.frames = resized_frames
|
|
207
|
+
num_colors = min(num_colors, 48) # More aggressive color limit for emoji
|
|
208
|
+
|
|
209
|
+
# More aggressive FPS reduction for emoji
|
|
210
|
+
if len(self.frames) > 12:
|
|
211
|
+
print(
|
|
212
|
+
f" Reducing frames from {len(self.frames)} to ~12 for emoji size"
|
|
213
|
+
)
|
|
214
|
+
# Keep every nth frame to get close to 12 frames
|
|
215
|
+
keep_every = max(1, len(self.frames) // 12)
|
|
216
|
+
self.frames = [
|
|
217
|
+
self.frames[i] for i in range(0, len(self.frames), keep_every)
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
# Optimize colors with global palette
|
|
221
|
+
optimized_frames = self.optimize_colors(num_colors, use_global_palette=True)
|
|
222
|
+
|
|
223
|
+
# Calculate frame duration in milliseconds
|
|
224
|
+
frame_duration = 1000 / self.fps
|
|
225
|
+
|
|
226
|
+
# Save GIF
|
|
227
|
+
imageio.imwrite(
|
|
228
|
+
output_path,
|
|
229
|
+
optimized_frames,
|
|
230
|
+
duration=frame_duration,
|
|
231
|
+
loop=0, # Infinite loop
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Get file info
|
|
235
|
+
file_size_kb = output_path.stat().st_size / 1024
|
|
236
|
+
file_size_mb = file_size_kb / 1024
|
|
237
|
+
|
|
238
|
+
info = {
|
|
239
|
+
"path": str(output_path),
|
|
240
|
+
"size_kb": file_size_kb,
|
|
241
|
+
"size_mb": file_size_mb,
|
|
242
|
+
"dimensions": f"{self.width}x{self.height}",
|
|
243
|
+
"frame_count": len(optimized_frames),
|
|
244
|
+
"fps": self.fps,
|
|
245
|
+
"duration_seconds": len(optimized_frames) / self.fps,
|
|
246
|
+
"colors": num_colors,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# Print info
|
|
250
|
+
print(f"\n✓ GIF created successfully!")
|
|
251
|
+
print(f" Path: {output_path}")
|
|
252
|
+
print(f" Size: {file_size_kb:.1f} KB ({file_size_mb:.2f} MB)")
|
|
253
|
+
print(f" Dimensions: {self.width}x{self.height}")
|
|
254
|
+
print(f" Frames: {len(optimized_frames)} @ {self.fps} fps")
|
|
255
|
+
print(f" Duration: {info['duration_seconds']:.1f}s")
|
|
256
|
+
print(f" Colors: {num_colors}")
|
|
257
|
+
|
|
258
|
+
# Size info
|
|
259
|
+
if optimize_for_emoji:
|
|
260
|
+
print(f" Optimized for emoji (128x128, reduced colors)")
|
|
261
|
+
if file_size_mb > 1.0:
|
|
262
|
+
print(f"\n Note: Large file size ({file_size_kb:.1f} KB)")
|
|
263
|
+
print(" Consider: fewer frames, smaller dimensions, or fewer colors")
|
|
264
|
+
|
|
265
|
+
return info
|
|
266
|
+
|
|
267
|
+
def clear(self):
|
|
268
|
+
"""Clear all frames (useful for creating multiple GIFs)."""
|
|
269
|
+
self.frames = []
|