auricui-skill 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +29 -2
- package/bin/install.mjs +66 -0
- package/package.json +29 -4
- package/skill/SKILL.md +64 -0
- package/skill/agents/openai.yaml +4 -0
- package/skill/assets/examples/ApprovalCard.tsx +8 -0
- package/skill/assets/examples/BasicApp.tsx +38 -0
- package/skill/assets/examples/BriefingCard.tsx +8 -0
- package/skill/assets/examples/ChatControls.tsx +49 -0
- package/skill/assets/examples/ChatPanel.tsx +73 -0
- package/skill/assets/examples/Checkbox.tsx +13 -0
- package/skill/assets/examples/Combobox.tsx +14 -0
- package/skill/assets/examples/Drawer.tsx +10 -0
- package/skill/assets/examples/MemoryCard.tsx +9 -0
- package/skill/assets/examples/MessageList.tsx +8 -0
- package/skill/assets/examples/Theme.tsx +14 -0
- package/skill/assets/examples/ToolCall.tsx +8 -0
- package/skill/assets/examples/Tree.tsx +15 -0
- package/skill/assets/examples/examples.css +11 -0
- package/skill/assets/library/auricui-0.1.0-preview.1.tgz +0 -0
- package/skill/assets/library/auricui-0.1.0-preview.3.tgz +0 -0
- package/skill/assets/library/auricui-0.1.0-preview.4.tgz +0 -0
- package/skill/assets/upstream/LICENSE +21 -0
- package/skill/assets/upstream/apps/desktop/chat/src/App.tsx +1099 -0
- package/skill/assets/upstream/apps/desktop/chat/src/app-bridge.ts +154 -0
- package/skill/assets/upstream/apps/desktop/chat/src/chat.css +1812 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/AddSkill.tsx +223 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/AgentActivityRow.tsx +716 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/AgentIcons.tsx +46 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/AttachmentCard.tsx +34 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/AttachmentPreview.tsx +331 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/BalanceNotice.tsx +22 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/BrandLogo.tsx +35 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/ComposerCard.tsx +182 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/ConversationSessionBar.tsx +292 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/CopyDiagnostics.tsx +24 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/Divider.tsx +47 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/FileChangesList.tsx +119 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/Markdown.tsx +198 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/ModelPopover.tsx +280 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/NoticeCard.tsx +87 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/Popover.tsx +195 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/PreviewFrame.tsx +129 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/ProjectConversation.tsx +852 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/ProjectStarters.tsx +118 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/Tooltip.tsx +191 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/TurnLog.tsx +583 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/TurnQuestionCard.tsx +133 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/dock-pane-bar.ts +14 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/project-version.ts +37 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/MentionMenuPlugin.tsx +372 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptEditor.tsx +476 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptPill.tsx +399 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptReferenceNode.tsx +164 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/index.ts +31 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/mention-menu.ts +206 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/prompt-text.ts +171 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/useStickToBottom.ts +320 -0
- package/skill/assets/upstream/apps/desktop/chat/src/components/useTicker.ts +55 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/index.ts +42 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/en-US.ts +408 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/es.ts +407 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/ja.ts +407 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/ko.ts +403 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/words.ts +6 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/zh-CN.ts +407 -0
- package/skill/assets/upstream/apps/desktop/chat/src/i18n/lookup.ts +12 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-activity-runs.ts +154 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-menu-rows.ts +41 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-refs.ts +105 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-transcript.ts +440 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/asset-lists.ts +75 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/attachment-place.ts +17 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/balance.ts +16 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-layer.ts +10 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-menu.ts +18 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-store.ts +101 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/composer-attachments.ts +189 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/desktop-attachments.ts +178 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/desktop-bridge.ts +291 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/developer.ts +39 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/esc-stop.ts +23 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/file-changes.ts +32 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/live-activity.ts +20 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/local-agent-run.ts +1174 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/local-sessions.ts +25 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/model-calls.ts +87 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/pretty-json.ts +12 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/preview-top.ts +15 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/project-resources.ts +66 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/project-sessions.ts +66 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/project-versions.ts +116 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/ref-pictures.ts +119 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/relative-time.ts +46 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/skill-pages.ts +29 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/step-file.ts +23 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/structural-share.ts +52 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/studio-film.ts +44 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/studio-transfer.ts +40 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/unprompted.ts +27 -0
- package/skill/assets/upstream/apps/desktop/chat/src/lib/upload-limits.ts +53 -0
- package/skill/assets/upstream/apps/desktop/chat/src/mount.tsx +19 -0
- package/skill/assets/upstream/apps/desktop/chat/src/state/local-agent-store.ts +296 -0
- package/skill/assets/upstream/apps/desktop/chat/src/studio-refs.ts +307 -0
- package/skill/assets/upstream/apps/desktop/package.json +51 -0
- package/skill/assets/upstream/apps/desktop/shell/bridge.ts +415 -0
- package/skill/assets/upstream/apps/desktop/shell/index.html +27 -0
- package/skill/assets/upstream/apps/desktop/shell/main.tsx +17 -0
- package/skill/assets/upstream/apps/desktop/shell/shell.css +17 -0
- package/skill/assets/upstream/apps/desktop/shell/vite.config.ts +65 -0
- package/skill/assets/upstream/apps/desktop/src/agent-definitions.mjs +31 -0
- package/skill/assets/upstream/packages/openfilm/LICENSE +21 -0
- package/skill/assets/upstream/packages/openfilm/package.json +88 -0
- package/skill/assets/upstream/packages/openfilm/src/film-doc.d.mts +125 -0
- package/skill/assets/upstream/packages/openfilm/src/film-doc.mjs +986 -0
- package/skill/assets/upstream/packages/openfilm/src/overrides.mjs +73 -0
- package/skill/assets/upstream/packages/openfilm/src/timeline.mjs +1280 -0
- package/skill/assets/upstream/packages/openfilm/studio/server/bridge.js +209 -0
- package/skill/assets/upstream/packages/openfilm/studio/server/film-subtitle.mjs +785 -0
- package/skill/assets/upstream/packages/openfilm/studio/server/ops.mjs +403 -0
- package/skill/assets/upstream/packages/openfilm/studio/server/stage.js +1649 -0
- package/skill/assets/upstream/packages/openfilm/studio/server/track-layout.mjs +43 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/App.tsx +156 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/api.ts +328 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/AudioMeters.tsx +188 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BlockThumbs.tsx +84 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BlockWaveform.tsx +201 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BrandLoadingScreen.tsx +56 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BrandLogo.tsx +25 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Card.tsx +89 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ColorPicker.tsx +204 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ConfirmDialog.tsx +96 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ContextMenu.tsx +422 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/DesktopDialog.tsx +271 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ExamplesGallery.tsx +372 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmFullscreenBar.tsx +147 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmInspector.tsx +2310 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmStageSelect.tsx +3296 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FocusNav.tsx +28 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FontFamilyPicker.tsx +293 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FramePicker.tsx +69 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/InspectorPanel.tsx +25 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Launcher.tsx +38 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/LayersPanel.tsx +277 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/LiveTime.tsx +101 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MediaPool.tsx +1413 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MediaPreview.tsx +188 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MenuSelect.tsx +161 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MgOutline.tsx +24 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/OperationHistory.tsx +184 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PaneDivider.tsx +292 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PasteAttrsDialog.tsx +69 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PlaybackMenu.tsx +110 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Popover.tsx +159 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectCover.tsx +60 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectHistoryControls.tsx +748 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectLayoutMenu.tsx +82 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectMenu.tsx +111 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectSources.tsx +587 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectView.tsx +3222 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectsLibrary.tsx +567 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectsView.tsx +1593 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PropertiesSection.tsx +418 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ResourceInspector.tsx +100 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SelectionToolbar.tsx +157 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SkillsLibrary.tsx +504 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SourcePreview.tsx +464 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SpendAsks.tsx +88 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/StageMaskOverlay.tsx +299 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/StageTransformFrame.tsx +246 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleInspector.module.css +46 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleInspector.tsx +884 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleOverlay.tsx +148 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleRow.tsx +419 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/TabStrip.tsx +188 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Timeline.tsx +4911 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/TimelineMarkers.tsx +329 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Toast.tsx +55 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Tooltip.tsx +193 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/UpdateButton.tsx +104 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ViewerBar.tsx +98 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/WhatsNew.tsx +147 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/dock-pane-bar.ts +16 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/export/ExportCenter.tsx +1502 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/export/ExportTray.tsx +284 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/film-stage-hit.ts +18 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/inspector-fields.tsx +863 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/inspector-look.tsx +1357 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/player-controls/controls-bar.tsx +397 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/player-controls/transport.tsx +294 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/replace-drop.ts +20 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/AgentsSection.tsx +613 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/CustomServices.tsx +104 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/DiagnosticsGroup.tsx +202 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ModelsSection.tsx +179 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ProviderMark.tsx +24 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ProvidersSection.tsx +177 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/SettingsModal.tsx +511 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/SettingsSelect.tsx +84 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ShortcutsSection.tsx +291 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/StorageGroup.tsx +119 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/theme.ts +47 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ui.tsx +139 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/use-settings.ts +10 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/stage-menu.tsx +283 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/timeline-head-extras.tsx +91 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/typing-target.ts +45 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-editor-tools.tsx +408 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-page-changes.ts +48 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-project-history.ts +113 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/useBlockDrag.ts +639 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/Preview.tsx +426 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/player.ts +558 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/preview-frames.ts +36 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/steps.ts +36 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/undo.ts +156 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/use-film.ts +212 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/use-preview-renders.ts +89 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/globals.css +559 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/en.ts +1901 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/es.ts +1900 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/index.ts +130 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/ja.ts +1900 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/ko.ts +1896 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/words.ts +6 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/zh-CN.ts +1901 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/agent-row.ts +21 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/asset-timeline.ts +469 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/audio-levels.ts +60 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/batch-map.ts +28 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/browser.ts +10 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/canvas-fit.ts +116 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/chat-marks.ts +114 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-attrs.ts +102 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-fade.ts +70 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-look.ts +520 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-mask.ts +536 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-replace.ts +60 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/component-props.ts +128 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/context-length.ts +12 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/desktop-build.ts +44 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/diagnostics.ts +68 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/double-press.ts +22 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/dropped-overrides.ts +139 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/export-jobs.ts +191 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/export-spec.ts +261 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/field-input.ts +155 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-clip-edits.ts +82 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-frame.ts +134 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-overrides.ts +107 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-shape.ts +172 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film.ts +80 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/font-menu.ts +236 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/font-styles.ts +129 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/freeze-frame.ts +49 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/host.ts +438 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/inspect-scrub.ts +26 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/keymap-store.ts +74 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/keymap.ts +529 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/layers-tree.ts +133 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/look.ts +83 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/loudness.ts +58 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/markers.ts +152 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-bar.ts +58 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-folders.ts +45 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-place.ts +102 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-queue.ts +107 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-selection.ts +64 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/menu-position.ts +77 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/mg-transform.ts +111 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/mixed.ts +3 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/page-changes.ts +45 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/picture-files.ts +19 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/preview-prefs.ts +46 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/preview-renders.ts +71 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-delete.ts +20 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-folders.ts +144 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-history.ts +188 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-sources.ts +283 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/projects-page.ts +40 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/provider-marks.ts +35 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/providers.ts +163 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/resource-drag.ts +131 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/retry-image.ts +17 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/saved-insert.ts +40 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/saved.ts +125 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/selection-toolbar.ts +158 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/shortcut-hint.ts +21 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/similar-layers.ts +46 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/solo.ts +19 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-clip.ts +60 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-crop.ts +232 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-gesture.ts +277 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-layers.ts +71 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-mask.ts +176 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-measure.ts +53 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-types.ts +155 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/studio-refs.ts +248 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/studio-version.ts +41 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/subtitle-cues.ts +238 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/subtitles.ts +263 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/tab-menu.ts +6 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/template-controls.ts +131 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/theme-changes.ts +31 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-cache.ts +63 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-cell.ts +8 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-frame.ts +76 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-pick.ts +42 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timecode.ts +161 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-clipboard.ts +279 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-drag.ts +589 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-edit.ts +1138 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-keys.ts +63 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-layout.ts +1038 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-nav.ts +160 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-prefs.ts +73 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-split.ts +40 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-view.ts +105 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/title.ts +39 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/track-heights.ts +55 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/track-lock.ts +27 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/transitions.ts +459 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/type-metrics.ts +83 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-film-subtitles.ts +111 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-fonts.ts +181 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-loaded-thumbs.ts +91 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-edits.ts +134 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-font.ts +51 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-style.ts +68 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/waveform-columns.ts +48 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/web-update.ts +41 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/whats-new.ts +401 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/workspace-resources.ts +373 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/src/main.tsx +10 -0
- package/skill/assets/upstream/packages/openfilm/studio/ui/vite.config.ts +12 -0
- package/skill/assets/upstream/packages/shared/package.json +45 -0
- package/skill/assets/upstream/packages/shared/src/agent-activity.ts +263 -0
- package/skill/assets/upstream/packages/shared/src/contract.ts +285 -0
- package/skill/assets/upstream/packages/shared/src/index.ts +17 -0
- package/skill/assets/upstream/packages/shared/src/markdown-blocks.ts +252 -0
- package/skill/assets/upstream/packages/shared/src/modality.ts +4 -0
- package/skill/assets/upstream/packages/shared/src/prompt-reference.ts +113 -0
- package/skill/assets/upstream/packages/shared/src/title.ts +76 -0
- package/skill/assets/upstream/packages/shared/src/turn.ts +68 -0
- package/skill/assets/upstream/packages/shared/src/video.ts +26 -0
- package/skill/references/agent-components.md +33 -0
- package/skill/references/api-index.json +2693 -0
- package/skill/references/chat-components.md +65 -0
- package/skill/references/components.md +23 -0
- package/skill/references/core-components.md +44 -0
- package/skill/references/dependencies.md +22 -0
- package/skill/references/esm-package.md +48 -0
- package/skill/references/integration.md +43 -0
- package/skill/references/library-manifest.json +12 -0
- package/skill/references/source-manifest.json +325 -0
- package/skill/references/studio-components.md +24 -0
- package/skill/references/theme-motion.md +49 -0
- package/skill/references/troubleshooting.md +27 -0
- package/skill/references/verification.md +13 -0
- package/skill/scripts/check_dependencies.py +109 -0
- package/skill/scripts/inspect_api.py +60 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an export makes, as the export dialog chooses it: platform presets, codecs, quality, sizes, the estimate of a
|
|
3
|
+
* file's size. The server (studio/server/exports.mjs) draws the same frames with the same arithmetic.
|
|
4
|
+
*
|
|
5
|
+
* No watermark and no end card: the product doesn't make them, so a spec has no place for them.
|
|
6
|
+
*/
|
|
7
|
+
import { filmFrameBox, type FilmFrameFill, type FilmFrameId } from './film-frame.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Video codecs. `png` is a sequence of frames (one zip). The four with transparency each have their place:
|
|
11
|
+
* prores4444 for editors and compositing, hevc_alpha for Apple's apps and Safari (a fraction of ProRes's size), vp9
|
|
12
|
+
* for the web, png for anything (game engines, 3D, Nuke), the biggest.
|
|
13
|
+
*/
|
|
14
|
+
export type ExportVideoCodec = 'h264' | 'hevc' | 'vp9' | 'prores422hq' | 'prores4444' | 'hevc_alpha' | 'png';
|
|
15
|
+
export const EXPORT_ALPHA_CODECS: readonly ExportVideoCodec[] = ['prores4444', 'hevc_alpha', 'vp9', 'png'];
|
|
16
|
+
|
|
17
|
+
export type ExportQuality = 'standard' | 'high' | 'master';
|
|
18
|
+
|
|
19
|
+
export const CODEC_LABEL: Record<ExportVideoCodec, string> = {
|
|
20
|
+
h264: 'H.264',
|
|
21
|
+
hevc: 'HEVC',
|
|
22
|
+
vp9: 'WebM VP9',
|
|
23
|
+
prores422hq: 'ProRes 422 HQ',
|
|
24
|
+
prores4444: 'ProRes 4444',
|
|
25
|
+
hevc_alpha: 'HEVC + Alpha',
|
|
26
|
+
png: 'PNG',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/* ── platform presets ──────────────────────────────────────────────────── */
|
|
30
|
+
|
|
31
|
+
export type ExportPresetGroup = 'landscape' | 'vertical' | 'feed';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A platform preset. Names are not translated (a platform's name is its name). Only limits known for sure are
|
|
35
|
+
* written (the longest video): a wrong limit is worse than none, since people cut their film to fit it.
|
|
36
|
+
*/
|
|
37
|
+
export interface ExportPreset {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
group: ExportPresetGroup;
|
|
41
|
+
frame: Exclude<FilmFrameId, 'native'>;
|
|
42
|
+
shortEdge: number;
|
|
43
|
+
/** The most frames a second the platform shows: a film is exported at its own rate up to it (presetFps). */
|
|
44
|
+
fps: number;
|
|
45
|
+
/** Vertical feeds are mostly watched muted: subtitles burned in by default. */
|
|
46
|
+
burnSubtitles: boolean;
|
|
47
|
+
/** The platform's longest video, in seconds. */
|
|
48
|
+
maxDurationSec?: number;
|
|
49
|
+
/** A platform of mainland China: offered (first) when Studio speaks Chinese, not otherwise. */
|
|
50
|
+
china?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const EXPORT_PRESETS: readonly ExportPreset[] = [
|
|
54
|
+
{ id: 'bilibili', name: '哔哩哔哩', group: 'landscape', frame: '16:9', shortEdge: 1080, fps: 60, burnSubtitles: false, china: true },
|
|
55
|
+
{ id: 'douyin', name: '抖音', group: 'vertical', frame: '9:16', shortEdge: 1080, fps: 30, burnSubtitles: true, china: true },
|
|
56
|
+
{ id: 'kuaishou', name: '快手', group: 'vertical', frame: '9:16', shortEdge: 1080, fps: 30, burnSubtitles: true, china: true },
|
|
57
|
+
{ id: 'channels', name: '视频号', group: 'vertical', frame: '9:16', shortEdge: 1080, fps: 30, burnSubtitles: true, china: true },
|
|
58
|
+
{ id: 'xiaohongshu', name: '小红书', group: 'feed', frame: '3:4', shortEdge: 1080, fps: 30, burnSubtitles: true, china: true },
|
|
59
|
+
{ id: 'youtube', name: 'YouTube', group: 'landscape', frame: '16:9', shortEdge: 1080, fps: 60, burnSubtitles: false },
|
|
60
|
+
{ id: 'youtube-4k', name: 'YouTube 4K', group: 'landscape', frame: '16:9', shortEdge: 2160, fps: 60, burnSubtitles: false },
|
|
61
|
+
{ id: 'x', name: 'X', group: 'landscape', frame: '16:9', shortEdge: 1080, fps: 30, burnSubtitles: true, maxDurationSec: 140 },
|
|
62
|
+
{ id: 'shorts', name: 'YouTube Shorts', group: 'vertical', frame: '9:16', shortEdge: 1080, fps: 30, burnSubtitles: true, maxDurationSec: 180 },
|
|
63
|
+
{ id: 'tiktok', name: 'TikTok', group: 'vertical', frame: '9:16', shortEdge: 1080, fps: 30, burnSubtitles: true },
|
|
64
|
+
{ id: 'reels', name: 'Instagram Reels', group: 'vertical', frame: '9:16', shortEdge: 1080, fps: 30, burnSubtitles: true },
|
|
65
|
+
{ id: 'instagram', name: 'Instagram Feed', group: 'feed', frame: '4:5', shortEdge: 1080, fps: 30, burnSubtitles: true },
|
|
66
|
+
{ id: 'square', name: 'LinkedIn · Facebook', group: 'feed', frame: '1:1', shortEdge: 1080, fps: 30, burnSubtitles: true },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/** The platforms offered in a language: China's first in Chinese, and none of them in any other. */
|
|
70
|
+
export function exportPresetsFor(language: string): readonly ExportPreset[] {
|
|
71
|
+
return language === 'zh-CN' ? [...EXPORT_PRESETS.filter((p) => p.china), ...EXPORT_PRESETS.filter((p) => !p.china)] : EXPORT_PRESETS.filter((p) => !p.china);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A film's frames a second on a platform: its own, up to the most the platform shows. A 30 fps film is not drawn
|
|
76
|
+
* twice over for a platform that shows 60: no frame of it would be new.
|
|
77
|
+
*/
|
|
78
|
+
export function presetFps(preset: ExportPreset, filmFps: number): number {
|
|
79
|
+
return filmFps > 0 ? Math.min(preset.fps, filmFps) : preset.fps;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function exportPreset(id: string | undefined): ExportPreset | null {
|
|
83
|
+
return EXPORT_PRESETS.find((p) => p.id === id) ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ── sizes ─────────────────────────────────────────────────────────────── */
|
|
87
|
+
|
|
88
|
+
export const EXPORT_MAX_DIM = 4096;
|
|
89
|
+
|
|
90
|
+
function even(n: number): number {
|
|
91
|
+
return Math.max(2, Math.round(n / 2) * 2);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A picture's size with its short side at `shortEdge`, the box's exact shape kept (`ratio`, when it has one: a 16:9
|
|
96
|
+
* frame at 1080 is 1920 × 1080); no side over 4096.
|
|
97
|
+
*/
|
|
98
|
+
export function exportDimsForShortEdge(box: { w: number; h: number; ratio?: number }, shortEdge: number): { w: number; h: number } {
|
|
99
|
+
const r = box.ratio ?? box.w / Math.max(1, box.h);
|
|
100
|
+
let w: number;
|
|
101
|
+
let h: number;
|
|
102
|
+
if (r >= 1) {
|
|
103
|
+
h = even(shortEdge);
|
|
104
|
+
w = even(h * r);
|
|
105
|
+
} else {
|
|
106
|
+
w = even(shortEdge);
|
|
107
|
+
h = even(w / r);
|
|
108
|
+
}
|
|
109
|
+
if (w > EXPORT_MAX_DIM || h > EXPORT_MAX_DIM) {
|
|
110
|
+
const k = Math.min(EXPORT_MAX_DIM / w, EXPORT_MAX_DIM / h);
|
|
111
|
+
w = even(w * k);
|
|
112
|
+
h = even(h * k);
|
|
113
|
+
}
|
|
114
|
+
return { w, h };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A GIF's size: its long side at `width`. */
|
|
118
|
+
export function exportGifDims(box: { w: number; h: number; ratio?: number }, width: number): { w: number; h: number } {
|
|
119
|
+
const r = box.ratio ?? box.w / Math.max(1, box.h);
|
|
120
|
+
return r >= 1 ? { w: even(width), h: even(width / r) } : { w: even(width * r), h: even(width) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The size of a framed picture. */
|
|
124
|
+
export function exportFramedDims(
|
|
125
|
+
stage: { w: number; h: number },
|
|
126
|
+
frame: FilmFrameId,
|
|
127
|
+
fill: FilmFrameFill,
|
|
128
|
+
shortEdge: number,
|
|
129
|
+
): { w: number; h: number } {
|
|
130
|
+
return exportDimsForShortEdge(filmFrameBox(stage, frame, fill), shortEdge);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ── bitrate and size ──────────────────────────────────────────────────── */
|
|
134
|
+
|
|
135
|
+
/** Bits per pixel per frame by quality, for the encoders given a bitrate (exports.mjs BPP and `bitrate`). */
|
|
136
|
+
const BITS_PER_PIXEL: Record<ExportQuality, number> = { standard: 0.07, high: 0.11, master: 0.2 };
|
|
137
|
+
|
|
138
|
+
/** The bitrate an encoder is given (the server's arithmetic): size × fps × quality × `k`, 2 to 120 Mbps. */
|
|
139
|
+
function givenBitrate(size: { w: number; h: number }, fps: number, quality: ExportQuality, k: number): number {
|
|
140
|
+
return Math.min(120_000_000, Math.max(2_000_000, size.w * size.h * Math.max(1, fps) * BITS_PER_PIXEL[quality] * k));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A file's size depends mostly on what is in the picture, not on the settings: at the same 1080p30 H.264 settings
|
|
145
|
+
* films measured run from about 0.6 Mbps (a slide of text) to 21 Mbps (a field of particles). So the dialog shows a
|
|
146
|
+
* range, from a typical film's picture: 0.25× to 4× of it for the codecs given a quality (CRF), ±10% for those near a
|
|
147
|
+
* fixed size a pixel. A rare busy film still falls above it.
|
|
148
|
+
*/
|
|
149
|
+
const TYPICAL_VIDEO = { w: 1920, h: 1080, fps: 30, bytes: 150_000 };
|
|
150
|
+
const VIDEO_SPREAD: readonly [number, number] = [0.25, 4];
|
|
151
|
+
const FIXED_SPREAD: readonly [number, number] = [0.9, 1.1];
|
|
152
|
+
|
|
153
|
+
/** How a CRF file's size follows the settings: the quality's CRF, the codec at its CRF. */
|
|
154
|
+
const QUALITY_SIZE: Record<ExportQuality, number> = { standard: 0.58, high: 1, master: 1.6 };
|
|
155
|
+
/* HEVC as libx265 encodes it, at its CRF (an ffmpeg without libx265 uses VideoToolbox, given a bitrate: larger) */
|
|
156
|
+
const CODEC_SIZE: Partial<Record<ExportVideoCodec, number>> = { h264: 1, hevc: 0.6, vp9: 0.7 };
|
|
157
|
+
/** The sound beside the picture, as render encodes it: AAC 192k in an MP4, Opus 160k in WebM, 48 kHz PCM in a MOV. */
|
|
158
|
+
const AUDIO_BYTES_PER_SECOND: Partial<Record<ExportVideoCodec, number>> = { vp9: 20_000, prores422hq: 192_000, prores4444: 192_000, hevc_alpha: 192_000 };
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* How many of a picture's pixels are the film's: bars cost an encoder almost nothing. The rest of the frame, with
|
|
162
|
+
* `contain`, is bars; with `cover` the film fills it.
|
|
163
|
+
*/
|
|
164
|
+
export function exportFilmPixels(stage: { w: number; h: number }, frame: FilmFrameId, fill: FilmFrameFill, size: { w: number; h: number }): number {
|
|
165
|
+
const box = filmFrameBox(stage, frame, fill);
|
|
166
|
+
const share = Math.min(1, (box.inner.w * box.inner.h) / Math.max(1, box.w * box.h));
|
|
167
|
+
return size.w * size.h * share;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A video file's likely size in bytes, `[low, high]`, for the dialog's "about 1 MB–10 MB": a typical film's picture
|
|
172
|
+
* scaled to the pixels of film in it, the frame rate (more frames, each more like the last), the quality and the codec;
|
|
173
|
+
* ProRes, PNG and HEVC with alpha by their own arithmetic. Plus the sound, at both ends.
|
|
174
|
+
*/
|
|
175
|
+
export function exportEstimateBytes(spec: {
|
|
176
|
+
codec: ExportVideoCodec;
|
|
177
|
+
size: { w: number; h: number };
|
|
178
|
+
/** the pixels of film in the picture (exportFilmPixels); without it, all of them */
|
|
179
|
+
filmPixels?: number;
|
|
180
|
+
fps: number;
|
|
181
|
+
quality: ExportQuality;
|
|
182
|
+
durationMs: number;
|
|
183
|
+
audio: boolean;
|
|
184
|
+
}): [number, number] {
|
|
185
|
+
const sec = Math.max(0, spec.durationMs) / 1000;
|
|
186
|
+
const px = spec.size.w * spec.size.h;
|
|
187
|
+
let videoBps: number;
|
|
188
|
+
let spread = FIXED_SPREAD;
|
|
189
|
+
switch (spec.codec) {
|
|
190
|
+
/* ProRes is a fixed-rate family: 422 HQ about 220 Mbps at 1080p30, 4444 about 330, scaled by pixels × fps */
|
|
191
|
+
case 'prores422hq': videoBps = 220_000_000 * (px / (1920 * 1080)) * (spec.fps / 30); break;
|
|
192
|
+
case 'prores4444': videoBps = 330_000_000 * (px / (1920 * 1080)) * (spec.fps / 30); break;
|
|
193
|
+
/* HEVC with alpha (VideoToolbox) is given its bitrate */
|
|
194
|
+
case 'hevc_alpha': videoBps = givenBitrate(spec.size, spec.fps, spec.quality, 0.8); break;
|
|
195
|
+
/* a PNG sequence of motion graphics compresses well: about a byte per pixel */
|
|
196
|
+
case 'png': videoBps = px * 8 * spec.fps; break;
|
|
197
|
+
default: {
|
|
198
|
+
const pixels = spec.filmPixels ?? px;
|
|
199
|
+
videoBps = 8 * TYPICAL_VIDEO.bytes * (pixels / (TYPICAL_VIDEO.w * TYPICAL_VIDEO.h)) ** 0.75
|
|
200
|
+
* (Math.max(1, spec.fps) / TYPICAL_VIDEO.fps) ** 0.35 * QUALITY_SIZE[spec.quality] * (CODEC_SIZE[spec.codec] ?? 1);
|
|
201
|
+
spread = VIDEO_SPREAD;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const audioBytes = spec.audio ? (AUDIO_BYTES_PER_SECOND[spec.codec] ?? 24_000) : 0;
|
|
205
|
+
const at = (k: number) => Math.round((videoBps / 8 * k + audioBytes) * sec);
|
|
206
|
+
return [at(spread[0]), at(spread[1])];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** A typical film's GIF, 640 × 360 at 15 fps, and how far a film's strays from it. */
|
|
210
|
+
const TYPICAL_GIF = { w: 640, h: 360, fps: 15, bytes: 300_000 };
|
|
211
|
+
const GIF_SPREAD: readonly [number, number] = [0.4, 3];
|
|
212
|
+
|
|
213
|
+
/** A GIF's likely size in bytes, `[low, high]`: a typical film's GIF scaled to the pixels of film and the frame rate. */
|
|
214
|
+
export function exportGifEstimateBytes(spec: {
|
|
215
|
+
size: { w: number; h: number };
|
|
216
|
+
filmPixels?: number;
|
|
217
|
+
fps: number;
|
|
218
|
+
durationMs: number;
|
|
219
|
+
}): [number, number] {
|
|
220
|
+
const pixels = spec.filmPixels ?? spec.size.w * spec.size.h;
|
|
221
|
+
const bytes = TYPICAL_GIF.bytes * (pixels / (TYPICAL_GIF.w * TYPICAL_GIF.h)) ** 0.9 * (Math.max(1, spec.fps) / TYPICAL_GIF.fps) ** 0.6
|
|
222
|
+
* Math.max(0, spec.durationMs) / 1000;
|
|
223
|
+
return [Math.round(bytes * GIF_SPREAD[0]), Math.round(bytes * GIF_SPREAD[1])];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** A place a part of the film can start or end, named by the clips that start (or end) there. */
|
|
227
|
+
export type RangeMark = { ms: number; title: string };
|
|
228
|
+
|
|
229
|
+
/** Clip edges this close together are one place to cut (a frame or two apart is the same cut to a person). */
|
|
230
|
+
const SAME_CUT_MS = 400;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Where a part of the film can start and end: at its picture clips' edges ("scenes two to four" is what people mean).
|
|
234
|
+
* A start is named by the clips that start there, an end by the clips that end there. Edges close together are one;
|
|
235
|
+
* a start goes to the earliest of them and an end to the latest, so the part takes in every clip named. The film's
|
|
236
|
+
* own start and end are always there (an edge close to one is it).
|
|
237
|
+
*/
|
|
238
|
+
export function rangeMarks(scenes: readonly { startMs: number; durMs: number; label: string }[], totalMs: number): { starts: RangeMark[]; ends: RangeMark[] } {
|
|
239
|
+
const shown = scenes.filter((sc) => sc.durMs > 0 && sc.startMs < totalMs);
|
|
240
|
+
const group = (edges: { ms: number; label: string }[], latest: boolean) => {
|
|
241
|
+
const sorted = [...edges].sort((a, b) => a.ms - b.ms);
|
|
242
|
+
const groups: { ms: number; last: number; labels: string[] }[] = [];
|
|
243
|
+
for (const e of sorted) {
|
|
244
|
+
const g = groups.at(-1);
|
|
245
|
+
if (g && e.ms - g.last < SAME_CUT_MS) {
|
|
246
|
+
g.last = e.ms;
|
|
247
|
+
if (latest) g.ms = e.ms;
|
|
248
|
+
if (!g.labels.includes(e.label)) g.labels.push(e.label);
|
|
249
|
+
} else groups.push({ ms: e.ms, last: e.ms, labels: [e.label] });
|
|
250
|
+
}
|
|
251
|
+
return groups.map((g) => ({ ms: g.ms, title: g.labels.filter(Boolean).join(', ') }));
|
|
252
|
+
};
|
|
253
|
+
const starts = group(shown.map((sc) => ({ ms: Math.max(0, sc.startMs), label: sc.label })), false);
|
|
254
|
+
const ends = group(shown.map((sc) => ({ ms: Math.min(totalMs, sc.startMs + sc.durMs), label: sc.label })), true);
|
|
255
|
+
if (starts[0] && starts[0].ms < SAME_CUT_MS) starts[0].ms = 0;
|
|
256
|
+
else starts.unshift({ ms: 0, title: '' });
|
|
257
|
+
const last = ends.at(-1);
|
|
258
|
+
if (last && totalMs - last.ms < SAME_CUT_MS) last.ms = totalMs;
|
|
259
|
+
else if (totalMs > 0) ends.push({ ms: totalMs, title: '' });
|
|
260
|
+
return { starts, ends: ends.filter((e) => e.ms > 0) };
|
|
261
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a person types into a number field of the inspector, and how it is read: arithmetic
|
|
3
|
+
* (`1440/2`), a change of the value there (`+20`, `-20`, `*2`, `/2`), and units — `px` is dropped, `50%` is half of
|
|
4
|
+
* what the field measures against (the stage, for a place or a size), and a time takes seconds (`2.5s`), frames
|
|
5
|
+
* (`12f`, at the timecode's rate), milliseconds and timecodes (`00:00:02:15`, read as the timecode shows them).
|
|
6
|
+
*
|
|
7
|
+
* A leading `-` is a change, like `+`: "20 to the left". A negative value itself is typed in brackets, `(-20)`, or
|
|
8
|
+
* worked out, `0-20`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { exactRate, timecodeSeconds } from './timecode.ts';
|
|
12
|
+
|
|
13
|
+
/** A value to set, or a change of the value there (each of several selected things changes from its own). */
|
|
14
|
+
export type FieldInput = { set: number } | { rel: '+' | '-' | '*' | '/'; by: number };
|
|
15
|
+
|
|
16
|
+
export interface FieldContext {
|
|
17
|
+
/**
|
|
18
|
+
* What `100%` is: a number (the stage's width for an X), or `'self'` when the field is a percentage itself (an
|
|
19
|
+
* opacity, a crop). Without it, `%` is not understood.
|
|
20
|
+
*/
|
|
21
|
+
percent?: number | 'self';
|
|
22
|
+
/** A time in seconds: `s`, `ms`, `f` and timecodes are understood (a bare number is seconds). */
|
|
23
|
+
time?: boolean;
|
|
24
|
+
/** Frames a second for `f` and timecodes. */
|
|
25
|
+
fps?: number;
|
|
26
|
+
/** The field's own unit marks, dropped when typed (`°`, `deg`, `×`, `x`). */
|
|
27
|
+
units?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Token =
|
|
31
|
+
| { kind: 'num'; value: number }
|
|
32
|
+
| { kind: 'op'; value: '+' | '-' | '*' | '/' | '(' | ')' };
|
|
33
|
+
|
|
34
|
+
const DEFAULT_FPS = 30;
|
|
35
|
+
|
|
36
|
+
/** The tokens of `raw`, units already applied to their numbers; null when something is not understood. */
|
|
37
|
+
function tokenize(raw: string, ctx: FieldContext, inScale: boolean): Token[] | null {
|
|
38
|
+
const fps = ctx.fps ?? DEFAULT_FPS;
|
|
39
|
+
const units = [...(ctx.units ?? [])].sort((a, b) => b.length - a.length);
|
|
40
|
+
const out: Token[] = [];
|
|
41
|
+
let i = 0;
|
|
42
|
+
const s = raw.replace(/[−–]/g, '-').replace(/,/g, '.');
|
|
43
|
+
while (i < s.length) {
|
|
44
|
+
const ch = s[i]!;
|
|
45
|
+
if (/\s/.test(ch)) { i += 1; continue; }
|
|
46
|
+
if ('+-*/()'.includes(ch)) { out.push({ kind: 'op', value: ch as '+' }); i += 1; continue; }
|
|
47
|
+
const m = /^(?:\d+(?:\.\d*)?|\.\d+)(?::\d+(?:\.\d*)?)*/.exec(s.slice(i));
|
|
48
|
+
if (!m) return null;
|
|
49
|
+
i += m[0].length;
|
|
50
|
+
const rest = s.slice(i);
|
|
51
|
+
const unit = /^(px|ms|s|f|%)/i.exec(rest)?.[1]?.toLowerCase() ?? units.find((u) => rest.toLowerCase().startsWith(u.toLowerCase()));
|
|
52
|
+
if (unit) i += unit.length;
|
|
53
|
+
let value: number;
|
|
54
|
+
if (m[0].includes(':')) {
|
|
55
|
+
/* a timecode: SS:FF, MM:SS:FF or HH:MM:SS:FF */
|
|
56
|
+
if (!ctx.time || unit) return null;
|
|
57
|
+
const parts = m[0].split(':').map(Number);
|
|
58
|
+
if (parts.length > 4) return null;
|
|
59
|
+
const frames = parts.pop()!;
|
|
60
|
+
const [sec = 0, min = 0, hour = 0] = parts.reverse();
|
|
61
|
+
value = timecodeSeconds(hour * 3600 + min * 60 + sec, frames, fps);
|
|
62
|
+
} else {
|
|
63
|
+
const n = Number(m[0]);
|
|
64
|
+
if (unit === 'px' || (unit && units.includes(unit))) value = n;
|
|
65
|
+
else if (unit === '%') {
|
|
66
|
+
/* a scale (`*50%`) is a fraction; otherwise a share of what the field measures against */
|
|
67
|
+
if (inScale) value = n / 100;
|
|
68
|
+
else if (ctx.percent === 'self') value = n;
|
|
69
|
+
else if (typeof ctx.percent === 'number') value = (n / 100) * ctx.percent;
|
|
70
|
+
else return null;
|
|
71
|
+
} else if (unit === 's' || unit === 'ms' || unit === 'f') {
|
|
72
|
+
if (!ctx.time) return null;
|
|
73
|
+
value = unit === 's' ? n : unit === 'ms' ? n / 1000 : n / exactRate(fps);
|
|
74
|
+
} else value = n;
|
|
75
|
+
}
|
|
76
|
+
out.push({ kind: 'num', value });
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Arithmetic over the tokens: + − × ÷, brackets, a sign before a number. */
|
|
82
|
+
function evaluate(tokens: Token[]): number | null {
|
|
83
|
+
let at = 0;
|
|
84
|
+
const peek = () => tokens[at];
|
|
85
|
+
const isOp = (v: string) => { const t = peek(); return t?.kind === 'op' && t.value === v; };
|
|
86
|
+
const factor = (): number | null => {
|
|
87
|
+
const t = peek();
|
|
88
|
+
if (!t) return null;
|
|
89
|
+
if (t.kind === 'num') { at += 1; return t.value; }
|
|
90
|
+
if (t.value === '-' || t.value === '+') {
|
|
91
|
+
at += 1;
|
|
92
|
+
const v = factor();
|
|
93
|
+
return v == null ? null : t.value === '-' ? -v : v;
|
|
94
|
+
}
|
|
95
|
+
if (t.value === '(') {
|
|
96
|
+
at += 1;
|
|
97
|
+
const v = expr();
|
|
98
|
+
if (v == null || !isOp(')')) return null;
|
|
99
|
+
at += 1;
|
|
100
|
+
return v;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
const term = (): number | null => {
|
|
105
|
+
let v = factor();
|
|
106
|
+
while (v != null && (isOp('*') || isOp('/'))) {
|
|
107
|
+
const op = (peek() as { value: string }).value;
|
|
108
|
+
at += 1;
|
|
109
|
+
const r = factor();
|
|
110
|
+
if (r == null) return null;
|
|
111
|
+
v = op === '*' ? v * r : r === 0 ? null : v / r;
|
|
112
|
+
}
|
|
113
|
+
return v;
|
|
114
|
+
};
|
|
115
|
+
const expr = (): number | null => {
|
|
116
|
+
let v = term();
|
|
117
|
+
while (v != null && (isOp('+') || isOp('-'))) {
|
|
118
|
+
const op = (peek() as { value: string }).value;
|
|
119
|
+
at += 1;
|
|
120
|
+
const r = term();
|
|
121
|
+
if (r == null) return null;
|
|
122
|
+
v = op === '+' ? v + r : v - r;
|
|
123
|
+
}
|
|
124
|
+
return v;
|
|
125
|
+
};
|
|
126
|
+
const v = expr();
|
|
127
|
+
return v != null && at === tokens.length && Number.isFinite(v) ? v : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** What was typed, or null when it does not read as a number. */
|
|
131
|
+
export function parseFieldInput(raw: string, ctx: FieldContext = {}): FieldInput | null {
|
|
132
|
+
const text = raw.trim().replace(/^[−–]/, '-');
|
|
133
|
+
if (!text) return null;
|
|
134
|
+
const lead = /^[+\-*/]/.exec(text)?.[0] as '+' | '-' | '*' | '/' | undefined;
|
|
135
|
+
if (lead) {
|
|
136
|
+
const tokens = tokenize(text.slice(1), ctx, lead === '*' || lead === '/');
|
|
137
|
+
const by = tokens && tokens.length ? evaluate(tokens) : null;
|
|
138
|
+
if (by == null || (lead === '/' && by === 0)) return null;
|
|
139
|
+
return { rel: lead, by };
|
|
140
|
+
}
|
|
141
|
+
const tokens = tokenize(text, ctx, false);
|
|
142
|
+
const value = tokens ? evaluate(tokens) : null;
|
|
143
|
+
return value == null ? null : { set: value };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The value `input` makes of `base`. */
|
|
147
|
+
export function applyFieldInput(input: FieldInput, base: number): number {
|
|
148
|
+
if ('set' in input) return input.set;
|
|
149
|
+
switch (input.rel) {
|
|
150
|
+
case '+': return base + input.by;
|
|
151
|
+
case '-': return base - input.by;
|
|
152
|
+
case '*': return base * input.by;
|
|
153
|
+
default: return base / input.by;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The edits the timeline and the Inspector write back to film.html: one property of one clip.
|
|
3
|
+
*
|
|
4
|
+
* A whole object is written (or removed), not a dotted path — film.html edits only know a clip's top-level keys.
|
|
5
|
+
* A box goes through stage-clip's compactBox first, so what follows from the picture is not written.
|
|
6
|
+
*/
|
|
7
|
+
import type { TimelineBlock } from './timeline-layout';
|
|
8
|
+
|
|
9
|
+
export type ClipEditValue = number | string | boolean | readonly number[] | Record<string, unknown> | null;
|
|
10
|
+
|
|
11
|
+
export interface ClipEdit {
|
|
12
|
+
loc: string;
|
|
13
|
+
prop: string;
|
|
14
|
+
value: ClipEditValue;
|
|
15
|
+
from?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** film.html's field names that the inspector calls otherwise, at the start of a refusal */
|
|
19
|
+
const FIELD_WORDS: Record<string, string> = { at: 'Start' };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A refused clip edit, said for a person: film.html's reader words its problems for an agent (`clip "c1" (a.mp4): #t=2,12
|
|
23
|
+
* ends past the file's length of 10 s (#t= is a part of the file, …)`). The clip's address and the asides go, the
|
|
24
|
+
* media fragment is called the trim, and the first sentence stays, starting with a capital.
|
|
25
|
+
*/
|
|
26
|
+
export function plainClipRefusal(message: string): string {
|
|
27
|
+
const text = message
|
|
28
|
+
.replace(/^[^:\n]*?\btracks\[\d+\](?:\.clips\[\d+\])?(?: "[^"]*")?:?\s*/, '')
|
|
29
|
+
.replace(/^(?:[\w.-]+:\d+:|clip "[^"]*" \([^)]*\):)\s*/, '')
|
|
30
|
+
.replace(/\s*\([^()]*\)/g, '')
|
|
31
|
+
.split(/\s+—\s+|(?<=\.)\s+/)[0]!
|
|
32
|
+
.replace(/^#t=([^,\s]+),([^\s]+)/, 'the trim $1–$2 s')
|
|
33
|
+
.replace(/^its part of the file\b/, 'the trim')
|
|
34
|
+
.trim();
|
|
35
|
+
if (!text) return message;
|
|
36
|
+
const field = /^([a-z]+)\b/.exec(text)?.[1];
|
|
37
|
+
const named = field && FIELD_WORDS[field] ? `${FIELD_WORDS[field]}${text.slice(field.length)}` : text;
|
|
38
|
+
const sentence = named.charAt(0).toUpperCase() + named.slice(1);
|
|
39
|
+
return /[.!?]$/.test(sentence) ? sentence : `${sentence}.`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Clips on one track do not overlap: a new start (the inspector's) that would put a clip over another on its track
|
|
44
|
+
* takes it to a new track just above, as a drag does. Edits that already say where the clip goes are left as they are.
|
|
45
|
+
*
|
|
46
|
+
* Every clip is checked where the whole batch puts it. Checked against where its neighbors were, three clips slid
|
|
47
|
+
* left together came apart: the second landed on the first's old place and went to a new track, the third after it.
|
|
48
|
+
* A trim (`at` with `start` or `end`) keeps its other edge, and the timeline already stops it at its neighbors.
|
|
49
|
+
*/
|
|
50
|
+
export function apartOnTracks(edits: ClipEdit[], blocks: readonly Pick<TimelineBlock, 'loc' | 'startMs' | 'endMs'>[]): ClipEdit[] {
|
|
51
|
+
const trackOf = (loc?: string) => (loc ? Number(/^film\.html#(\d+)\./.exec(loc)?.[1] ?? NaN) : NaN);
|
|
52
|
+
const atOf = new Map<string, number>();
|
|
53
|
+
const trimmed = new Set<string>();
|
|
54
|
+
const leaving = new Set<string>();
|
|
55
|
+
for (const e of edits) {
|
|
56
|
+
if (e.prop === 'at' && typeof e.value === 'number') atOf.set(e.loc, e.value * 1000);
|
|
57
|
+
if (e.prop === 'start' || e.prop === 'end') trimmed.add(e.loc);
|
|
58
|
+
if (e.prop === 'track') leaving.add(e.loc);
|
|
59
|
+
}
|
|
60
|
+
const landing = (b: Pick<TimelineBlock, 'loc' | 'startMs' | 'endMs'>) => {
|
|
61
|
+
const at = b.loc ? atOf.get(b.loc) : undefined;
|
|
62
|
+
if (at == null) return { startMs: b.startMs, endMs: b.endMs };
|
|
63
|
+
return { startMs: at, endMs: trimmed.has(b.loc!) ? b.endMs : at + (b.endMs - b.startMs) };
|
|
64
|
+
};
|
|
65
|
+
/* times are kept to the millisecond, a start and a length each rounded alone: clips that touch can overlap by one */
|
|
66
|
+
const TOUCH_MS = 1;
|
|
67
|
+
const out = [...edits];
|
|
68
|
+
for (const e of edits) {
|
|
69
|
+
if (e.prop !== 'at' || typeof e.value !== 'number' || leaving.has(e.loc) || trimmed.has(e.loc)) continue;
|
|
70
|
+
const self = blocks.find((b) => b.loc === e.loc);
|
|
71
|
+
const track = trackOf(e.loc);
|
|
72
|
+
if (!self || !Number.isFinite(track)) continue;
|
|
73
|
+
const me = landing(self);
|
|
74
|
+
const over = blocks.some((b) => {
|
|
75
|
+
if (b === self || !b.loc || leaving.has(b.loc) || trackOf(b.loc) !== track) return false;
|
|
76
|
+
const r = landing(b);
|
|
77
|
+
return me.startMs < r.endMs - TOUCH_MS && me.endMs > r.startMs + TOUCH_MS;
|
|
78
|
+
});
|
|
79
|
+
if (over) out.push({ loc: e.loc, prop: 'track', value: { insert: track } });
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivery frame: fitting a film into a box of another aspect ratio.
|
|
3
|
+
*
|
|
4
|
+
* The film stays as it is; the frame goes around it. A 16:9 film delivered vertically gets a 9:16 box with the film
|
|
5
|
+
* centered and black bars above and below — not a 1080×1920 stage, which would break every px coordinate the film was
|
|
6
|
+
* laid out with. Re-laying a film out is its author's job, not a button's.
|
|
7
|
+
*
|
|
8
|
+
* Two ways to fit it: `contain` (default) scales and centers with bars and never crops; `cover` fills the box and
|
|
9
|
+
* crops the overflow, so the edges are really gone — a choice, never the default.
|
|
10
|
+
*
|
|
11
|
+
* The frame is the export's choice, not part of the film: picked in the export dialog for one export (the film's own
|
|
12
|
+
* shape is its canvas, which the viewer's ratio button changes).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A delivery frame: `native` (no frame, the film at its own size), or a ratio written `W:H` ("16:9", "2.39:1"): one of
|
|
17
|
+
* the presets, or any other the person types, from 1:4 to 4:1.
|
|
18
|
+
*/
|
|
19
|
+
export type FilmFrameId = 'native' | `${number}:${number}`;
|
|
20
|
+
|
|
21
|
+
/** The presets, as editors offer them: video and social shapes first, then photo and cinema ones. */
|
|
22
|
+
export const FILM_FRAME_IDS: FilmFrameId[] = [
|
|
23
|
+
'native', '16:9', '9:16', '1:1', '4:5', '4:3', '3:4', '3:2', '2:3', '21:9', '2.39:1', '1.85:1', '2:1',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/** How far a frame may go from square, either way (a custom one past it is refused). */
|
|
27
|
+
export const FRAME_RATIO_LIMIT = 4;
|
|
28
|
+
const RATIO = /^(\d{1,4}(?:\.\d{1,3})?):(\d{1,4}(?:\.\d{1,3})?)$/;
|
|
29
|
+
|
|
30
|
+
/** Width ÷ height of a frame written `W:H`, or null when it is not one (or too far from square). */
|
|
31
|
+
export function frameRatioOf(frame: string): number | null {
|
|
32
|
+
const m = RATIO.exec(frame);
|
|
33
|
+
if (!m) return null;
|
|
34
|
+
const ratio = Number(m[1]) / Number(m[2]);
|
|
35
|
+
return Number.isFinite(ratio) && ratio >= 1 / FRAME_RATIO_LIMIT && ratio <= FRAME_RATIO_LIMIT ? ratio : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A frame from a typed width and height ("2.39" and "1"), or null when they make none. */
|
|
39
|
+
export function customFilmFrame(w: number, h: number): FilmFrameId | null {
|
|
40
|
+
const num = (n: number) => String(Math.round(n * 1000) / 1000);
|
|
41
|
+
if (!(w > 0) || !(h > 0)) return null;
|
|
42
|
+
const id = `${num(w)}:${num(h)}`;
|
|
43
|
+
return frameRatioOf(id) != null ? id as FilmFrameId : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function filmFrameRatio(frame: FilmFrameId, stage: { w: number; h: number }): number {
|
|
47
|
+
return (frame === 'native' ? null : frameRatioOf(frame)) ?? stage.w / Math.max(1, stage.h);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Even sizes: H.264 samples chroma in 2×2 blocks and refuses odd dimensions. */
|
|
51
|
+
function even(n: number): number {
|
|
52
|
+
return Math.max(2, Math.round(n / 2) * 2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type FilmFrameFill = 'contain' | 'cover';
|
|
56
|
+
|
|
57
|
+
export interface FilmFrameBox {
|
|
58
|
+
/** The frame's pixel size (an export comes out this size). */
|
|
59
|
+
w: number;
|
|
60
|
+
h: number;
|
|
61
|
+
/** Its exact shape: the frame's own ratio, not its even-rounded pixels' (16:9 around a 2:1 film is 2160 × 1216). */
|
|
62
|
+
ratio: number;
|
|
63
|
+
/** The film inside the frame, scaled. With `cover`, `left`/`top` go negative: the overflow is cut by the frame. */
|
|
64
|
+
inner: { w: number; h: number; left: number; top: number; scale: number };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Fit `stage` into `frame`: the frame's size and the film's place in it. The frame's long side follows the film's
|
|
69
|
+
* (a 4K film framed 9:16 stays 4K-sharp; a 720p film is not blown up), so the frame only decides the shape.
|
|
70
|
+
*/
|
|
71
|
+
export function filmFrameBox(
|
|
72
|
+
stage: { w: number; h: number },
|
|
73
|
+
frame: FilmFrameId,
|
|
74
|
+
fill: FilmFrameFill = 'contain',
|
|
75
|
+
): FilmFrameBox {
|
|
76
|
+
const sw = Math.max(1, stage.w);
|
|
77
|
+
const sh = Math.max(1, stage.h);
|
|
78
|
+
const ratio = frame === 'native' ? null : frameRatioOf(frame);
|
|
79
|
+
if (ratio == null) {
|
|
80
|
+
return { w: even(sw), h: even(sh), ratio: sw / sh, inner: { w: sw, h: sh, left: 0, top: 0, scale: 1 } };
|
|
81
|
+
}
|
|
82
|
+
/* the frame must hold the film both ways: take the larger */
|
|
83
|
+
const byWidth = { w: sw, h: sw / ratio };
|
|
84
|
+
const byHeight = { w: sh * ratio, h: sh };
|
|
85
|
+
const box = byWidth.h >= sh ? byWidth : byHeight;
|
|
86
|
+
const w = even(box.w);
|
|
87
|
+
const h = even(box.h);
|
|
88
|
+
const scale = fill === 'cover'
|
|
89
|
+
? Math.max(w / sw, h / sh)
|
|
90
|
+
: Math.min(w / sw, h / sh);
|
|
91
|
+
const iw = sw * scale;
|
|
92
|
+
const ih = sh * scale;
|
|
93
|
+
return {
|
|
94
|
+
w,
|
|
95
|
+
h,
|
|
96
|
+
ratio,
|
|
97
|
+
inner: { w: iw, h: ih, left: (w - iw) / 2, top: (h - ih) / 2, scale },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Whether the frame has the film's own shape (then framing it changes nothing). */
|
|
102
|
+
export function filmFrameIsNative(stage: { w: number; h: number }, frame: FilmFrameId): boolean {
|
|
103
|
+
const ratio = frame === 'native' ? null : frameRatioOf(frame);
|
|
104
|
+
if (ratio == null) return true;
|
|
105
|
+
return Math.abs(stage.w / Math.max(1, stage.h) - ratio) < 0.01;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The stage itself in another shape (canvas mode: nothing has been laid out yet, so changing the stage breaks
|
|
110
|
+
* nothing). 1080p family: the short side is pinned at 1080 (16:9 = 1920×1080, 21:9 = 2520×1080, 9:16 = 1080×1920).
|
|
111
|
+
*/
|
|
112
|
+
const HD = 1080;
|
|
113
|
+
|
|
114
|
+
export function filmFrameStage(
|
|
115
|
+
stage: { w: number; h: number },
|
|
116
|
+
frame: FilmFrameId,
|
|
117
|
+
): { w: number; h: number } {
|
|
118
|
+
const ratio = frame === 'native' ? null : frameRatioOf(frame);
|
|
119
|
+
if (ratio == null) return { w: even(stage.w), h: even(stage.h) };
|
|
120
|
+
return ratio >= 1
|
|
121
|
+
? { w: even(HD * ratio), h: even(HD) }
|
|
122
|
+
: { w: even(HD), h: even(HD / ratio) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Which frame this stage's shape is (`native` when none). Canvas mode ticks this one. */
|
|
126
|
+
export function filmFrameOf(stage: { w: number; h: number }): FilmFrameId {
|
|
127
|
+
return FILM_FRAME_IDS.find((id) => id !== 'native' && filmFrameIsNative(stage, id)) ?? 'native';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function parseFilmFrame(raw: unknown): FilmFrameId {
|
|
131
|
+
return typeof raw === 'string' && (raw === 'native' || frameRatioOf(raw) != null)
|
|
132
|
+
? raw as FilmFrameId
|
|
133
|
+
: 'native';
|
|
134
|
+
}
|