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,1649 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Studio's stage inside the film's timeline page (src/timeline.mjs), injected after bridge.js. The editor runs on
|
|
3
|
+
* another origin and cannot touch the picture's DOM, so what the stage needs of it lives here: what is under a point,
|
|
4
|
+
* what a layer is (its kind, words, style, override, parents), where it is drawn, a move / scale / turn / style / text
|
|
5
|
+
* shown before it is kept, the in-place text box, and the keys pressed while the picture has focus.
|
|
6
|
+
*
|
|
7
|
+
* Nothing here writes film.html: a change is only shown (as a preview of the clip's `overrides`, or a clip placed in a
|
|
8
|
+
* box) until the editor keeps it, and the film's next update takes the shown change back.
|
|
9
|
+
*
|
|
10
|
+
* editor → { source: 'openfilm-studio', type: 'stage', seq, op, ...args } (ops below, at `ops`)
|
|
11
|
+
* film → { source: 'openfilm-film', type: 'stage', seq, result } the answer (null when nothing)
|
|
12
|
+
* { type: 'stage-event', event: 'typing', handle, value } the words of an in-place edit as they are typed
|
|
13
|
+
* { type: 'stage-event', event: 'text', handle, commit, value?, before? } an in-place edit ended
|
|
14
|
+
* { type: 'stage-event', event: 'key', key, code, repeat, metaKey, ctrlKey, shiftKey, altKey }
|
|
15
|
+
* a key pressed in the picture (not while typing in it)
|
|
16
|
+
*
|
|
17
|
+
* Points and boxes are in stage px (the film's own size, before the editor scales its frame). A layer is named by a
|
|
18
|
+
* `handle` (a number, good while its element is in the page) and by `loc`: its selector, with `#n` when the selector
|
|
19
|
+
* finds several (the override target `{ at, n }`).
|
|
20
|
+
*
|
|
21
|
+
* Only the editor's origin is listened to and answered (the server puts it in for EDITOR).
|
|
22
|
+
*/
|
|
23
|
+
(() => {
|
|
24
|
+
const EDITOR = __EDITOR__;
|
|
25
|
+
const BLUE = '#0d99ff';
|
|
26
|
+
const post = (message) => parent.postMessage({ source: 'openfilm-film', ...message }, EDITOR);
|
|
27
|
+
const editor = () => window.__filmEditor;
|
|
28
|
+
const hooks = () => window.__filmEditor?.stage;
|
|
29
|
+
const clips = () => hooks()?.clips() ?? [];
|
|
30
|
+
|
|
31
|
+
/* ── handles: a number per element, kept weakly ── */
|
|
32
|
+
let nextHandle = 1;
|
|
33
|
+
const byHandle = new Map();
|
|
34
|
+
const handles = new WeakMap();
|
|
35
|
+
const handleOf = (el) => {
|
|
36
|
+
let h = handles.get(el);
|
|
37
|
+
if (!h) {
|
|
38
|
+
h = nextHandle++;
|
|
39
|
+
handles.set(el, h);
|
|
40
|
+
byHandle.set(h, new WeakRef(el));
|
|
41
|
+
if (byHandle.size > 4000) for (const [k, ref] of byHandle) if (!ref.deref()?.isConnected) byHandle.delete(k);
|
|
42
|
+
}
|
|
43
|
+
return h;
|
|
44
|
+
};
|
|
45
|
+
const nodeOf = (h) => {
|
|
46
|
+
if (h == null) return null;
|
|
47
|
+
const el = byHandle.get(h)?.deref();
|
|
48
|
+
return el?.isConnected ? el : null;
|
|
49
|
+
};
|
|
50
|
+
const clipOfNode = (el) => clips().find((c) => c.kind === 'page' && c.win === el?.ownerDocument?.defaultView) ?? null;
|
|
51
|
+
const clipById = (id) => clips().find((c) => c.id === id) ?? null;
|
|
52
|
+
|
|
53
|
+
/* ── a clip on the stage: its placement maps its own px to stage px ── */
|
|
54
|
+
const matrixOf = (c) => new DOMMatrix(getComputedStyle(c.el).transform);
|
|
55
|
+
const mapPoint = (M, x, y) => { const p = M.transformPoint(new DOMPoint(x, y)); return { x: p.x, y: p.y }; };
|
|
56
|
+
/** A box in the clip's px → the stage box around it. */
|
|
57
|
+
const mapRect = (M, r) => {
|
|
58
|
+
const pts = [[r.left, r.top], [r.left + r.width, r.top], [r.left + r.width, r.top + r.height], [r.left, r.top + r.height]]
|
|
59
|
+
.map(([x, y]) => mapPoint(M, x, y));
|
|
60
|
+
const xs = pts.map((p) => p.x);
|
|
61
|
+
const ys = pts.map((p) => p.y);
|
|
62
|
+
const left = Math.min(...xs);
|
|
63
|
+
const top = Math.min(...ys);
|
|
64
|
+
return { left, top, width: Math.max(...xs) - left, height: Math.max(...ys) - top };
|
|
65
|
+
};
|
|
66
|
+
/** A frame (center, size, turn) in the clip's px → the same frame on the stage (the clip's turn and scale added). */
|
|
67
|
+
const mapFrame = (M, f) => {
|
|
68
|
+
const rad = (f.r * Math.PI) / 180;
|
|
69
|
+
const ux = { x: Math.cos(rad), y: Math.sin(rad) };
|
|
70
|
+
const uy = { x: -Math.sin(rad), y: Math.cos(rad) };
|
|
71
|
+
const at = (kx, ky) => mapPoint(M, f.cx + (ux.x * kx * f.w + uy.x * ky * f.h) / 2, f.cy + (ux.y * kx * f.w + uy.y * ky * f.h) / 2);
|
|
72
|
+
const c = mapPoint(M, f.cx, f.cy);
|
|
73
|
+
const p0 = at(-1, -1);
|
|
74
|
+
const p1 = at(1, -1);
|
|
75
|
+
const p3 = at(-1, 1);
|
|
76
|
+
const r = (Math.atan2(p1.y - p0.y, p1.x - p0.x) * 180) / Math.PI;
|
|
77
|
+
return { cx: c.x, cy: c.y, w: Math.hypot(p1.x - p0.x, p1.y - p0.y), h: Math.hypot(p3.x - p0.x, p3.y - p0.y), r: Math.abs(r) < 1e-6 ? 0 : r };
|
|
78
|
+
};
|
|
79
|
+
const clipTurn = (M) => (Math.atan2(M.b, M.a) * 180) / Math.PI;
|
|
80
|
+
const clipScale = (M) => Math.hypot(M.a, M.b) || 1;
|
|
81
|
+
|
|
82
|
+
/** The picture clips a person sees now, the top one first (track 0 is on top). */
|
|
83
|
+
const shown = () => clips()
|
|
84
|
+
.filter((c) => c.el && c.kind !== 'sound' && c.el.style.visibility === 'visible' && !(Number(c.el.style.opacity || 1) <= 0))
|
|
85
|
+
.sort((a, b) => a.ti - b.ti);
|
|
86
|
+
/** The top clip under a stage point, by its placed box (a full-frame video draws nothing a point test could find). */
|
|
87
|
+
const clipAt = (x, y, pictures = false) => {
|
|
88
|
+
for (const c of shown()) {
|
|
89
|
+
if (pictures && c.kind === 'page') continue;
|
|
90
|
+
const p = matrixOf(c).inverse().transformPoint(new DOMPoint(x, y));
|
|
91
|
+
if (p.x >= 0 && p.y >= 0 && p.x <= c.w && p.y <= c.h) return c;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/* ── a page's layers (ported from the old Studio's stage-dom / stage-inspect) ── */
|
|
97
|
+
const SKIP = new Set(['script', 'style', 'link', 'meta', 'template', 'noscript', 'title', 'head', 'br', 'wbr']);
|
|
98
|
+
const IMAGE_TAGS = new Set(['img', 'video', 'canvas', 'picture']);
|
|
99
|
+
const SHAPE_TAGS = new Set(['svg', 'path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'g', 'text', 'tspan', 'use']);
|
|
100
|
+
const SVG_TEXT_TAGS = new Set(['text', 'tspan', 'textpath']);
|
|
101
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
102
|
+
const INLINE = new Set(['BR', 'WBR', 'EM', 'STRONG', 'B', 'I', 'U', 'S', 'SPAN', 'SMALL', 'SUP', 'SUB', 'A', 'MARK', 'CODE', 'ABBR', 'Q', 'TIME']);
|
|
103
|
+
const EDITOR_ATTR = 'data-openfilm-text-editor';
|
|
104
|
+
const LIMIT = 1500;
|
|
105
|
+
/** At most this many custom properties, and as many words, as a page's template controls. */
|
|
106
|
+
const CONTROLS = 40;
|
|
107
|
+
|
|
108
|
+
const view = (el) => el.ownerDocument.defaultView;
|
|
109
|
+
const css = (el) => view(el).getComputedStyle(el);
|
|
110
|
+
const tagKind = (tag) => (IMAGE_TAGS.has(tag) ? 'image' : SHAPE_TAGS.has(tag) ? 'shape' : null);
|
|
111
|
+
const isSvgText = (el) => el.namespaceURI === SVG_NS && SVG_TEXT_TAGS.has(el.localName.toLowerCase());
|
|
112
|
+
const isGroup = (el) => Boolean(el?.hasAttribute?.('data-group'));
|
|
113
|
+
const kebab = (key) => key.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
|
|
114
|
+
|
|
115
|
+
/** Every element of a page that can be a layer, in document order. */
|
|
116
|
+
const layersOf = (doc) => {
|
|
117
|
+
const out = [];
|
|
118
|
+
if (!doc.body) return out;
|
|
119
|
+
for (const el of doc.body.querySelectorAll('*')) {
|
|
120
|
+
if (out.length >= LIMIT) break;
|
|
121
|
+
if (SKIP.has(el.localName.toLowerCase()) || el.hasAttribute(EDITOR_ATTR) || el.closest(`[${EDITOR_ATTR}]`)) continue;
|
|
122
|
+
out.push(el);
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Faded out to nothing, here or on the way up: not hit, not framed. */
|
|
128
|
+
const invisible = (el) => {
|
|
129
|
+
for (let node = el; node; node = node.parentElement) {
|
|
130
|
+
if (!(Number.parseFloat(css(node).opacity) > 0)) return true;
|
|
131
|
+
if (node === el.ownerDocument.documentElement) break;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const collapse = (raw) => raw.replace(/[^\S\n]+/g, ' ').replace(/ *\n */g, '\n').trim();
|
|
137
|
+
/** The words on an element as written, line breaks kept (`<br>` and block children both break a line). */
|
|
138
|
+
const stageText = (el) => {
|
|
139
|
+
let out = '';
|
|
140
|
+
el.childNodes.forEach((node) => {
|
|
141
|
+
if (node.nodeType === 3) { out += node.nodeValue ?? ''; return; }
|
|
142
|
+
if (node.nodeType !== 1) return;
|
|
143
|
+
if (node.localName === 'br') { out += '\n'; return; }
|
|
144
|
+
const display = css(node).display;
|
|
145
|
+
if (!display.startsWith('inline') && display !== 'contents' && out && !out.endsWith('\n')) out += '\n';
|
|
146
|
+
out += stageText(node);
|
|
147
|
+
});
|
|
148
|
+
return collapse(out);
|
|
149
|
+
};
|
|
150
|
+
/** An element's own words: all of them, or, when it holds inline parts (`<em>`, `<br>`…), those written in it. */
|
|
151
|
+
const ownWords = (el) => [...el.childNodes].filter((n) => n.nodeType === 3).map((n) => n.data).join('').replace(/\s+/g, ' ').trim();
|
|
152
|
+
/* an icon beside the words (a badge's ✓, a button's arrow) does not stop them being the element's text: its own
|
|
153
|
+
words are typed in place, and the icon stays (a text override changes the element's own words only) */
|
|
154
|
+
const ICON = new Set(['svg', 'img']);
|
|
155
|
+
/** Words alone, or lines between <br>s: a `text` override changes all of them, a <br> at each line break. */
|
|
156
|
+
const onlyBreaks = (el) => [...el.children].every((ch) => ch.localName === 'br');
|
|
157
|
+
/* words of its own beside blocks of their own (a title with its subtitle written inside it) are its text too: the
|
|
158
|
+
blocks are layers of their own, and a text override changes its own words only */
|
|
159
|
+
const textOf = (el) => (onlyBreaks(el) ? stageText(el) : ownWords(el));
|
|
160
|
+
/** The blocks written inside a line of words (a subtitle inside its title): layers of their own. */
|
|
161
|
+
const blocksOf = (el) => [...el.children].filter((ch) => !INLINE.has(ch.tagName) && !ICON.has(ch.localName) && !SKIP.has(ch.localName.toLowerCase()));
|
|
162
|
+
/** Those shown now: still shown while the line's own words are typed. */
|
|
163
|
+
const blocksIn = (el) => blocksOf(el).filter((ch) => css(ch).visibility === 'visible');
|
|
164
|
+
/**
|
|
165
|
+
* A line of words with blocks written inside it: its own words are the layer (its box hugs them, the inspector shows
|
|
166
|
+
* them), and the blocks in it stay where they are and as they look when it is moved or restyled (holdBlocks).
|
|
167
|
+
*/
|
|
168
|
+
const mixedText = (el) => el.namespaceURI !== SVG_NS && !onlyBreaks(el) && blocksOf(el).length > 0 && Boolean(ownWords(el));
|
|
169
|
+
/** The box of an element's own words on its page (the union of their lines' boxes). */
|
|
170
|
+
const wordsRect = (el) => {
|
|
171
|
+
const range = el.ownerDocument.createRange();
|
|
172
|
+
let l = Infinity; let t = Infinity; let r = -Infinity; let b = -Infinity;
|
|
173
|
+
for (const n of el.childNodes) {
|
|
174
|
+
if (n.nodeType !== 3 || !n.data.trim()) continue;
|
|
175
|
+
range.selectNodeContents(n);
|
|
176
|
+
for (const x of range.getClientRects()) {
|
|
177
|
+
if (x.width < 0.5 && x.height < 0.5) continue;
|
|
178
|
+
l = Math.min(l, x.left); t = Math.min(t, x.top); r = Math.max(r, x.right); b = Math.max(b, x.bottom);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (l < r && t < b) return { left: l, top: t, width: r - l, height: b - t };
|
|
182
|
+
const x = el.getBoundingClientRect();
|
|
183
|
+
return { left: x.left, top: x.top, width: x.width, height: x.height };
|
|
184
|
+
};
|
|
185
|
+
/** The size of an element's own words as laid out, before any transform (each run measured in a span of its own). */
|
|
186
|
+
const wordsSize = (el) => {
|
|
187
|
+
const doc = el.ownerDocument;
|
|
188
|
+
let l = Infinity; let t = Infinity; let r = -Infinity; let b = -Infinity;
|
|
189
|
+
for (const n of [...el.childNodes]) {
|
|
190
|
+
if (n.nodeType !== 3 || !n.data.trim()) continue;
|
|
191
|
+
const span = doc.createElement('span');
|
|
192
|
+
n.before(span);
|
|
193
|
+
span.append(n);
|
|
194
|
+
l = Math.min(l, span.offsetLeft); t = Math.min(t, span.offsetTop);
|
|
195
|
+
r = Math.max(r, span.offsetLeft + span.offsetWidth); b = Math.max(b, span.offsetTop + span.offsetHeight);
|
|
196
|
+
span.replaceWith(n);
|
|
197
|
+
}
|
|
198
|
+
return l < r && t < b ? { w: r - l, h: b - t } : { w: el.offsetWidth, h: el.offsetHeight };
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const transparent = (color) => {
|
|
202
|
+
const c = color.trim();
|
|
203
|
+
if (!c || c === 'transparent') return true;
|
|
204
|
+
const m = /^rgba\([^)]*,\s*([\d.]+)\s*\)$/.exec(c);
|
|
205
|
+
return m != null && Number(m[1]) === 0;
|
|
206
|
+
};
|
|
207
|
+
/** Whether a layer paints anything itself: a fill, a border, a background picture, a shadow. */
|
|
208
|
+
const paints = (cs) => !transparent(cs.backgroundColor)
|
|
209
|
+
|| (cs.backgroundImage && cs.backgroundImage !== 'none')
|
|
210
|
+
|| (Number.parseFloat(cs.borderTopWidth) > 0 && !transparent(cs.borderTopColor))
|
|
211
|
+
|| (cs.boxShadow && cs.boxShadow !== 'none')
|
|
212
|
+
|| (Number.parseFloat(cs.outlineWidth) > 0 && cs.outlineStyle !== 'none' && !transparent(cs.outlineColor))
|
|
213
|
+
|| (cs.backdropFilter && cs.backdropFilter !== 'none');
|
|
214
|
+
/** Whether the person changed how it looks in Studio (an override's style is written with priority): theirs to
|
|
215
|
+
select again, though it paints nothing now (its fill taken off) */
|
|
216
|
+
const restyled = (el) => [...el.style].some((p) => el.style.getPropertyPriority(p) === 'important');
|
|
217
|
+
|
|
218
|
+
/** text, image, shape or box; null for an empty shell (a click there goes to what is under it). */
|
|
219
|
+
const kindOf = (el) => {
|
|
220
|
+
if (isSvgText(el) && textOf(el)) return 'text';
|
|
221
|
+
const byTag = tagKind(el.localName.toLowerCase());
|
|
222
|
+
if (byTag) return byTag;
|
|
223
|
+
if (el.namespaceURI !== SVG_NS && textOf(el)) return 'text';
|
|
224
|
+
return paints(css(el)) || restyled(el) ? 'box' : null;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const angleDeg = (raw) => {
|
|
228
|
+
const v = Number.parseFloat(raw);
|
|
229
|
+
if (!Number.isFinite(v)) return 0;
|
|
230
|
+
if (/rad$/i.test(raw)) return (v * 180) / Math.PI;
|
|
231
|
+
if (/turn$/i.test(raw)) return v * 360;
|
|
232
|
+
if (/grad$/i.test(raw)) return v * 0.9;
|
|
233
|
+
return v;
|
|
234
|
+
};
|
|
235
|
+
const transformTurn = (transform) => {
|
|
236
|
+
if (!transform || transform === 'none') return 0;
|
|
237
|
+
const m = /matrix(3d)?\(([^)]+)\)/i.exec(transform);
|
|
238
|
+
if (!m) return 0;
|
|
239
|
+
const n = m[2].split(',').map((x) => Number.parseFloat(x));
|
|
240
|
+
return Number.isFinite(n[0]) && Number.isFinite(n[1]) ? (Math.atan2(n[1], n[0]) * 180) / Math.PI : 0;
|
|
241
|
+
};
|
|
242
|
+
const ownTurn = (el) => { const r = css(el).rotate; return r && r !== 'none' ? angleDeg(r.split(/\s+/).pop()) : 0; };
|
|
243
|
+
/** How far a layer and everything around it in its page is turned (the `rotate` property and `transform`). */
|
|
244
|
+
const totalTurn = (el) => {
|
|
245
|
+
let r = 0;
|
|
246
|
+
for (let node = el; node && node.nodeType === 1; node = node.parentElement) r += ownTurn(node) + transformTurn(css(node).transform);
|
|
247
|
+
return r;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/** The override geometry painted on a layer now (inline translate / scale / rotate). */
|
|
251
|
+
const paintedOf = (el) => {
|
|
252
|
+
const tr = el.style.getPropertyValue('translate').trim();
|
|
253
|
+
const sc = el.style.getPropertyValue('scale').trim();
|
|
254
|
+
const ro = el.style.getPropertyValue('rotate').trim();
|
|
255
|
+
const px = tr && tr !== 'none' ? tr.split(/\s+/).map((v) => Number.parseFloat(v) || 0) : [];
|
|
256
|
+
const sp = sc && sc !== 'none' ? sc.split(/\s+/).map((v) => Number.parseFloat(v) || 1) : [];
|
|
257
|
+
return { t: [px[0] ?? 0, px[1] ?? 0], s: [sp[0] ?? 1, sp[1] ?? sp[0] ?? 1], r: ro && ro !== 'none' ? angleDeg(ro) : 0 };
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/** A layer's box in its page. A group's is the box of what is in it (it fills the frame and paints nothing). */
|
|
261
|
+
const rectOf = (el) => {
|
|
262
|
+
if (isGroup(el)) {
|
|
263
|
+
let l = Infinity; let t = Infinity; let r = -Infinity; let b = -Infinity;
|
|
264
|
+
for (const c of el.querySelectorAll('*')) {
|
|
265
|
+
if (isGroup(c) || SKIP.has(c.localName.toLowerCase()) || invisible(c)) continue;
|
|
266
|
+
const x = c.getBoundingClientRect();
|
|
267
|
+
if (x.width < 1 || x.height < 1) continue;
|
|
268
|
+
l = Math.min(l, x.left); t = Math.min(t, x.top); r = Math.max(r, x.right); b = Math.max(b, x.bottom);
|
|
269
|
+
}
|
|
270
|
+
if (l < r && t < b) return { left: l, top: t, width: r - l, height: b - t };
|
|
271
|
+
}
|
|
272
|
+
if (mixedText(el)) return wordsRect(el);
|
|
273
|
+
const x = el.getBoundingClientRect();
|
|
274
|
+
return { left: x.left, top: x.top, width: x.width, height: x.height };
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* A layer's frame in its page as drawn: center from its box, true width and height worked out from its laid-out
|
|
279
|
+
* aspect (times the override's scale) and its turn, so whatever scaled it is counted.
|
|
280
|
+
*/
|
|
281
|
+
const measureFrame = (el, s, r) => {
|
|
282
|
+
const mixed = mixedText(el);
|
|
283
|
+
const box = mixed ? wordsRect(el) : el.getBoundingClientRect();
|
|
284
|
+
const cx = box.left + box.width / 2;
|
|
285
|
+
const cy = box.top + box.height / 2;
|
|
286
|
+
if (mixed && Math.abs(r % 360) < 0.01) return { cx, cy, w: box.width, h: box.height, r: 0 };
|
|
287
|
+
let { w: lw, h: lh } = mixed ? wordsSize(el) : { w: el.offsetWidth, h: el.offsetHeight };
|
|
288
|
+
if (!(lw > 0 && lh > 0)) {
|
|
289
|
+
try { const bb = el.getBBox?.(); lw = bb?.width ?? 0; lh = bb?.height ?? 0; } catch { lw = 0; lh = 0; }
|
|
290
|
+
}
|
|
291
|
+
if (!(lw > 0 && lh > 0)) return { cx, cy, w: box.width, h: box.height, r: 0 };
|
|
292
|
+
const ratio = (lw * s[0]) / (lh * s[1]);
|
|
293
|
+
const rad = (r * Math.PI) / 180;
|
|
294
|
+
const c = Math.abs(Math.cos(rad));
|
|
295
|
+
const sn = Math.abs(Math.sin(rad));
|
|
296
|
+
const h = (box.width + box.height) / (ratio * c + sn + ratio * sn + c);
|
|
297
|
+
return { cx, cy, w: ratio * h, h, r };
|
|
298
|
+
};
|
|
299
|
+
/** The frame a layer is drawn in (turned with it and its parents): hover, hit and the selection box share it. */
|
|
300
|
+
const frameOf = (el, rect = rectOf(el)) => {
|
|
301
|
+
const r = totalTurn(el);
|
|
302
|
+
if (Math.abs(r % 360) < 0.01 || isGroup(el)) return { cx: rect.left + rect.width / 2, cy: rect.top + rect.height / 2, w: rect.width, h: rect.height, r: 0 };
|
|
303
|
+
return measureFrame(el, paintedOf(el).s, r);
|
|
304
|
+
};
|
|
305
|
+
const inFrame = (f, x, y) => {
|
|
306
|
+
if (!f.r) return Math.abs(x - f.cx) <= f.w / 2 && Math.abs(y - f.cy) <= f.h / 2;
|
|
307
|
+
const rad = (-f.r * Math.PI) / 180;
|
|
308
|
+
const dx = x - f.cx;
|
|
309
|
+
const dy = y - f.cy;
|
|
310
|
+
return Math.abs(dx * Math.cos(rad) - dy * Math.sin(rad)) <= f.w / 2 + 0.5 && Math.abs(dx * Math.sin(rad) + dy * Math.cos(rad)) <= f.h / 2 + 0.5;
|
|
311
|
+
};
|
|
312
|
+
/** Override px → page px: `translate` is in the parent's coordinates, so the parent's own scale counts. */
|
|
313
|
+
const localScale = (el) => {
|
|
314
|
+
for (let p = el.parentElement; p; p = p.parentElement) {
|
|
315
|
+
const r = p.getBoundingClientRect();
|
|
316
|
+
if (p.offsetWidth > 0 && r.width > 0) return r.width / p.offsetWidth;
|
|
317
|
+
}
|
|
318
|
+
return 1;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/* ── selectors: a name a person's tweak keeps pointing at when the page is rewritten around it ── */
|
|
322
|
+
const stepOf = (el) => {
|
|
323
|
+
const tag = el.localName;
|
|
324
|
+
const classes = [...el.classList].filter((c) => /^[A-Za-z_][\w-]*$/.test(c)).slice(0, 3);
|
|
325
|
+
const named = tag + classes.map((c) => `.${CSS.escape(c)}`).join('');
|
|
326
|
+
const siblings = [...el.parentElement.children];
|
|
327
|
+
if (siblings.filter((c) => c.tagName === el.tagName && classes.every((k) => c.classList.contains(k))).length === 1) return named;
|
|
328
|
+
const typed = siblings.filter((c) => c.tagName === el.tagName);
|
|
329
|
+
return `${named}:nth-of-type(${typed.indexOf(el) + 1})`;
|
|
330
|
+
};
|
|
331
|
+
const selectorOf = (el) => {
|
|
332
|
+
const doc = el.ownerDocument;
|
|
333
|
+
if (el.id && doc.querySelectorAll(`#${CSS.escape(el.id)}`).length === 1) return `#${CSS.escape(el.id)}`;
|
|
334
|
+
const parentEl = el.parentElement;
|
|
335
|
+
if (!parentEl || el === doc.body) return el.localName;
|
|
336
|
+
return `${parentEl === doc.body ? 'body' : selectorOf(parentEl)} > ${stepOf(el)}`;
|
|
337
|
+
};
|
|
338
|
+
const matches = (doc, o) => {
|
|
339
|
+
let nodes = [];
|
|
340
|
+
try { nodes = [...doc.querySelectorAll(o.at)]; } catch { return []; }
|
|
341
|
+
return o.n ? nodes.slice(o.n - 1, o.n) : nodes;
|
|
342
|
+
};
|
|
343
|
+
/** Its selector, and which match it is when the selector finds several. */
|
|
344
|
+
const locOf = (el) => {
|
|
345
|
+
const at = selectorOf(el);
|
|
346
|
+
const all = matches(el.ownerDocument, { at });
|
|
347
|
+
if (all.length <= 1) return { at, loc: at };
|
|
348
|
+
const n = all.indexOf(el) + 1;
|
|
349
|
+
return { at, n, loc: `${at}#${n}`, instance: n, instances: all.length };
|
|
350
|
+
};
|
|
351
|
+
const targetOf = (el) => { const { at, n } = locOf(el); return n ? { at, n } : { at }; };
|
|
352
|
+
/** Layers like it, wherever they are: its tag and classes (`h2.title`), or null when it has no class to go by. */
|
|
353
|
+
const similarOf = (el) => {
|
|
354
|
+
const classes = [...el.classList].filter((c) => /^[A-Za-z_][\w-]*$/.test(c)).slice(0, 3);
|
|
355
|
+
return classes.length ? el.localName + classes.map((c) => `.${CSS.escape(c)}`).join('') : null;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/** The clip's kept override that applies to this element (the last one that finds it). */
|
|
359
|
+
const overrideOf = (c, el) => {
|
|
360
|
+
let found = null;
|
|
361
|
+
for (const o of c.overrides ?? []) if (matches(el.ownerDocument, o).includes(el)) found = o;
|
|
362
|
+
if (!found) return null;
|
|
363
|
+
const out = {};
|
|
364
|
+
if (Array.isArray(found.t)) out.t = [Number(found.t[0]) || 0, Number(found.t[1]) || 0];
|
|
365
|
+
if (found.s != null) out.s = found.s;
|
|
366
|
+
if (typeof found.r === 'number') out.r = found.r;
|
|
367
|
+
if (found.style && typeof found.style === 'object') out.style = found.style;
|
|
368
|
+
if (found.lock === true) out.lock = true;
|
|
369
|
+
if (typeof found.text === 'string') out.text = found.text;
|
|
370
|
+
return Object.keys(out).length ? out : null;
|
|
371
|
+
};
|
|
372
|
+
/** Locked here or on a layer around it: not dragged, not boxed, not hovered. */
|
|
373
|
+
const lockedOf = (c, el) => {
|
|
374
|
+
const locks = (c.overrides ?? []).filter((o) => o.lock === true);
|
|
375
|
+
if (!locks.length) return false;
|
|
376
|
+
const held = new Set(locks.flatMap((o) => matches(el.ownerDocument, o)));
|
|
377
|
+
for (let node = el; node; node = node.parentElement) if (held.has(node)) return true;
|
|
378
|
+
return false;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/* what arrange (below) needs to know of a layer and its parents */
|
|
382
|
+
const FLEX_OR_GRID = /^(inline-)?(flex|grid)$/;
|
|
383
|
+
const stackingContext = (node) => {
|
|
384
|
+
/* the root only: a body with a background of its own would hide a layer sent behind it */
|
|
385
|
+
if (node === node.ownerDocument.documentElement) return true;
|
|
386
|
+
const cs = css(node);
|
|
387
|
+
/* a flex or grid item with a z-index is one too (zOf, below) */
|
|
388
|
+
return cs.isolation === 'isolate' || (cs.position !== 'static' && cs.zIndex !== 'auto') || Number.parseFloat(cs.opacity) < 1
|
|
389
|
+
|| cs.transform !== 'none' || cs.translate !== 'none' || cs.scale !== 'none' || cs.rotate !== 'none'
|
|
390
|
+
|| cs.filter !== 'none' || cs.mixBlendMode !== 'normal' || cs.clipPath !== 'none'
|
|
391
|
+
|| (cs.maskImage || cs.webkitMaskImage || 'none') !== 'none' || zOf(node) != null;
|
|
392
|
+
};
|
|
393
|
+
/** Whether a z-index on it counts: it has a position, or it is laid out by a flex or grid parent. */
|
|
394
|
+
const placedIn = (node) => css(node).position !== 'static' || FLEX_OR_GRID.test(css(node.parentElement ?? node).display);
|
|
395
|
+
/** The z-index it is drawn by, null for auto (a z-index where it does not count is auto). */
|
|
396
|
+
const zOf = (node) => {
|
|
397
|
+
if (!placedIn(node)) return null;
|
|
398
|
+
const z = Number.parseInt(css(node).zIndex, 10);
|
|
399
|
+
return Number.isFinite(z) ? z : null;
|
|
400
|
+
};
|
|
401
|
+
/**
|
|
402
|
+
* Where a layer is drawn in the stacking context it is in, as CSS paints them: by z-index (auto is 0); at the same
|
|
403
|
+
* z, those without a position first, then those with one and the stacking contexts made otherwise (a transform, an
|
|
404
|
+
* opacity, an isolation); then in document order (compare `order` last).
|
|
405
|
+
*/
|
|
406
|
+
const paintKey = (node) => {
|
|
407
|
+
const z = zOf(node);
|
|
408
|
+
if (z == null) return [0, css(node).position !== 'static' || stackingContext(node) ? 1 : 0];
|
|
409
|
+
return [z, z < 0 ? 0 : z === 0 ? 1 : 2];
|
|
410
|
+
};
|
|
411
|
+
const paintCompare = (a, b) => {
|
|
412
|
+
const ka = paintKey(a);
|
|
413
|
+
const kb = paintKey(b);
|
|
414
|
+
return ka[0] - kb[0] || ka[1] - kb[1] || (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
|
|
415
|
+
};
|
|
416
|
+
/**
|
|
417
|
+
* The part of a layer drawn last: the layer itself when it is a stacking context (all of it is drawn at once), else
|
|
418
|
+
* the latest of it and what is in it, since those are drawn among its siblings (a transformed card inside a plain
|
|
419
|
+
* column is drawn with the positioned layers, after a sibling column with a z-index of 0). `memo`: per reading.
|
|
420
|
+
*/
|
|
421
|
+
const reachOf = (node, memo) => {
|
|
422
|
+
let best = memo.get(node);
|
|
423
|
+
if (best) return best;
|
|
424
|
+
best = node;
|
|
425
|
+
if (!stackingContext(node)) {
|
|
426
|
+
for (const ch of node.children) {
|
|
427
|
+
if (SKIP.has(ch.localName.toLowerCase())) continue;
|
|
428
|
+
const r = reachOf(ch, memo);
|
|
429
|
+
if (paintCompare(best, r) < 0) best = r;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
memo.set(node, best);
|
|
433
|
+
return best;
|
|
434
|
+
};
|
|
435
|
+
/** Siblings in the order they are drawn, by what of each is drawn last; a fresh reading each time. */
|
|
436
|
+
const siblingOrder = (nodes) => {
|
|
437
|
+
const memo = new Map();
|
|
438
|
+
return [...nodes].sort((a, b) => paintCompare(reachOf(a, memo), reachOf(b, memo)));
|
|
439
|
+
};
|
|
440
|
+
/** whether giving `node` a position would move what is placed inside it against something further out */
|
|
441
|
+
const unsafe = (node) => [...node.querySelectorAll('*')].some((inner) => {
|
|
442
|
+
if (css(inner).position !== 'absolute') return false;
|
|
443
|
+
for (let up = inner.parentElement; up && up !== node; up = up.parentElement) if (css(up).position !== 'static') return false;
|
|
444
|
+
return true;
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
/* styles the inspector shows per kind (computed: a page states none as source keys) */
|
|
448
|
+
const TEXT_STYLE_KEYS = ['fontFamily', 'fontWeight', 'fontStyle', 'fontSize', 'lineHeight', 'letterSpacing', 'textAlign', 'color', 'opacity'];
|
|
449
|
+
const SVG_TEXT_STYLE_KEYS = ['fontFamily', 'fontWeight', 'fontStyle', 'fontSize', 'letterSpacing', 'fill', 'opacity'];
|
|
450
|
+
const KIND_STYLE_KEYS = {
|
|
451
|
+
text: [...TEXT_STYLE_KEYS, 'textTransform', 'textDecorationLine'],
|
|
452
|
+
image: ['opacity', 'borderRadius', 'objectFit', 'objectPosition', 'clipPath'],
|
|
453
|
+
shape: ['opacity', 'fill'],
|
|
454
|
+
box: ['backgroundColor', 'borderRadius', 'opacity', 'clipPath', 'boxShadow'],
|
|
455
|
+
};
|
|
456
|
+
/* the look every kind shows (blend, adjustments and shadow, stroke, faded edges, what is behind blurred): as the
|
|
457
|
+
page draws it, its CSS's or its own */
|
|
458
|
+
const LOOK_STYLE_KEYS = ['mixBlendMode', 'filter', 'border', 'outline', 'outlineOffset', 'backdropFilter', 'maskImage', 'maskComposite'];
|
|
459
|
+
const LOOK_KEYS = new Set([...LOOK_STYLE_KEYS, 'textTransform', 'textDecorationLine', 'objectPosition', 'clipPath', 'boxShadow']);
|
|
460
|
+
const UNITLESS = new Set(['opacity', 'fontWeight', 'lineHeight', 'zIndex', 'flex', 'flexGrow', 'flexShrink', 'order', 'zoom', 'scale', 'fillOpacity', 'strokeOpacity', 'strokeMiterlimit']);
|
|
461
|
+
const isColorKey = (key) => /^(color|fill|stroke|background|backgroundColor|borderColor|outlineColor|stopColor)$/.test(key) || /Color$/.test(key);
|
|
462
|
+
const pxNumber = (value) => {
|
|
463
|
+
const m = /^(-?[\d.]+)px$/.exec(String(value).trim());
|
|
464
|
+
if (!m) return undefined;
|
|
465
|
+
const n = Number(m[1]);
|
|
466
|
+
return Number.isFinite(n) ? Math.round(n * 10) / 10 : undefined;
|
|
467
|
+
};
|
|
468
|
+
/** `rgb(…)` / `rgba(…)` as hex; a see-through color keeps its alpha (`#00ff0080`), as the inspector wrote it. */
|
|
469
|
+
const rgbToHex = (color) => {
|
|
470
|
+
const m = /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/.exec(color.trim());
|
|
471
|
+
if (!m) return color.startsWith('#') ? color : undefined;
|
|
472
|
+
const hex = (n) => Number(n).toString(16).padStart(2, '0');
|
|
473
|
+
const a = m[4] == null ? 1 : Number(m[4]);
|
|
474
|
+
return `#${[m[1], m[2], m[3]].map(hex).join('')}${a < 1 ? hex(Math.round(a * 255)) : ''}`;
|
|
475
|
+
};
|
|
476
|
+
const own = (el, name) => el.style.getPropertyValue(name).trim();
|
|
477
|
+
const styleValue = (el, key, cs) => {
|
|
478
|
+
const name = kebab(key);
|
|
479
|
+
if (key === 'lineHeight') {
|
|
480
|
+
const mine = own(el, 'line-height');
|
|
481
|
+
if (mine && !mine.endsWith('px')) { const n = Number(mine); return Number.isFinite(n) ? n : mine; }
|
|
482
|
+
const size = pxNumber(cs.fontSize);
|
|
483
|
+
const leading = pxNumber(mine || cs.lineHeight);
|
|
484
|
+
return leading != null && size ? Math.round((leading / size) * 100) / 100 : undefined;
|
|
485
|
+
}
|
|
486
|
+
if (isColorKey(key)) {
|
|
487
|
+
const mine = own(el, name);
|
|
488
|
+
if (mine && /gradient|url\(/i.test(mine)) return mine;
|
|
489
|
+
return rgbToHex(mine || cs.getPropertyValue(name)) ?? (mine || undefined);
|
|
490
|
+
}
|
|
491
|
+
if (key === 'fontWeight') return own(el, 'font-weight') || cs.fontWeight || undefined;
|
|
492
|
+
if (key === 'fontFamily') return own(el, 'font-family') || cs.fontFamily || undefined;
|
|
493
|
+
if (key === 'fontStyle') { const v = own(el, 'font-style') || cs.fontStyle; return v && v !== 'normal' ? v : undefined; }
|
|
494
|
+
if (key === 'textAlign') { const v = own(el, 'text-align') || cs.textAlign; return v && v !== 'start' ? v : undefined; }
|
|
495
|
+
if (LOOK_KEYS.has(key)) {
|
|
496
|
+
const v = own(el, name) || cs.getPropertyValue(name);
|
|
497
|
+
/* an outline is computed color first: "rgb(0, 0, 0) none 0px" is none */
|
|
498
|
+
if (key === 'outline' && /\bnone\b/.test(v)) return undefined;
|
|
499
|
+
return v && !/^(none|normal|add|50% 50%)$/.test(v) && !/^0px none\b/.test(v) ? v : undefined;
|
|
500
|
+
}
|
|
501
|
+
if (UNITLESS.has(key)) {
|
|
502
|
+
const mine = own(el, name);
|
|
503
|
+
const n = Number(mine || cs.getPropertyValue(name));
|
|
504
|
+
return Number.isFinite(n) ? Math.round(n * 1000) / 1000 : (mine || undefined);
|
|
505
|
+
}
|
|
506
|
+
const mine = own(el, name);
|
|
507
|
+
if (!mine || /^-?[\d.]+px$/.test(mine) || mine === '0') { const n = pxNumber(mine || cs.getPropertyValue(name)); if (n != null) return n; }
|
|
508
|
+
return mine || undefined;
|
|
509
|
+
};
|
|
510
|
+
const styleOf = (el, kind) => {
|
|
511
|
+
const style = {};
|
|
512
|
+
const cs = css(el);
|
|
513
|
+
let keys = [...(kind === 'text' && isSvgText(el) ? SVG_TEXT_STYLE_KEYS : KIND_STYLE_KEYS[kind]), ...LOOK_STYLE_KEYS];
|
|
514
|
+
/* a line with its own fill (a badge, a tag) is a box too */
|
|
515
|
+
if (kind === 'text' && paints(cs)) keys.push('backgroundColor', 'borderRadius', 'boxShadow');
|
|
516
|
+
keys = [...new Set(keys)];
|
|
517
|
+
for (const key of keys) {
|
|
518
|
+
if (key === 'backgroundColor' && transparent(cs.backgroundColor)) continue;
|
|
519
|
+
if (key === 'objectFit') { const fit = own(el, 'object-fit') || cs.objectFit; if (fit) style[key] = fit; continue; }
|
|
520
|
+
if (key === 'borderRadius') {
|
|
521
|
+
const mine = own(el, 'border-radius');
|
|
522
|
+
/* four corners not alike: all four, as the shorthand writes them (top left, top right, bottom right, left) */
|
|
523
|
+
const corners = [cs.borderTopLeftRadius, cs.borderTopRightRadius, cs.borderBottomRightRadius, cs.borderBottomLeftRadius];
|
|
524
|
+
if (!mine && new Set(corners).size > 1 && corners.every((c) => pxNumber(c) != null)) { style[key] = corners.join(' '); continue; }
|
|
525
|
+
const n = pxNumber(mine || cs.borderTopLeftRadius);
|
|
526
|
+
if (n != null) style[key] = n; else if (mine) style[key] = mine;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const value = styleValue(el, key, cs);
|
|
530
|
+
if (value != null && value !== '') style[key] = value;
|
|
531
|
+
}
|
|
532
|
+
/* words drawn in a fill of their page's own (-webkit-text-fill-color: gold or gradient words), not in their color */
|
|
533
|
+
if (kind === 'text' && !isSvgText(el) && ownTextFill(el)) style.WebkitTextFillColor = styleValue(el, 'WebkitTextFillColor', cs);
|
|
534
|
+
return style;
|
|
535
|
+
};
|
|
536
|
+
/**
|
|
537
|
+
* Whether `el`'s words are drawn in a fill of their own rather than their color. A fill that follows the color
|
|
538
|
+
* reads as that color, so the color is changed for a moment (nothing is drawn in between) to see if it follows.
|
|
539
|
+
*/
|
|
540
|
+
const ownTextFill = (el) => {
|
|
541
|
+
const was = el.getAttribute('style');
|
|
542
|
+
el.style.setProperty('color', 'rgb(1, 2, 3)', 'important');
|
|
543
|
+
const fill = css(el).webkitTextFillColor;
|
|
544
|
+
if (was == null) el.removeAttribute('style'); else el.setAttribute('style', was);
|
|
545
|
+
return fill !== 'rgb(1, 2, 3)';
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const labelOf = (el, kind) => {
|
|
549
|
+
if (kind === 'text') {
|
|
550
|
+
const text = textOf(el).replace(/\s+/g, ' ');
|
|
551
|
+
if (text) return text.length <= 32 ? text : `${text.slice(0, 32)}…`;
|
|
552
|
+
}
|
|
553
|
+
if (kind === 'image') {
|
|
554
|
+
const src = el.getAttribute('src') ?? '';
|
|
555
|
+
const name = src.startsWith('data:') ? null : src.split(/[?#]/)[0].split('/').pop();
|
|
556
|
+
if (name) { try { return decodeURIComponent(name); } catch { return name; } }
|
|
557
|
+
}
|
|
558
|
+
if ((kind === 'box' || kind === 'shape') && el.querySelectorAll('*').length <= 1) {
|
|
559
|
+
const text = (el.textContent ?? '').trim().replace(/\s+/g, ' ');
|
|
560
|
+
if (text && text.length <= 24) return text;
|
|
561
|
+
}
|
|
562
|
+
return el.localName.toLowerCase();
|
|
563
|
+
};
|
|
564
|
+
/** The layers around this one in its page (nearest first), at most four: the inspector's breadcrumb. */
|
|
565
|
+
const parentsOf = (el) => {
|
|
566
|
+
const out = [];
|
|
567
|
+
for (let node = el.parentElement; node && node !== el.ownerDocument.body && out.length < 4; node = node.parentElement) {
|
|
568
|
+
const kind = kindOf(node) ?? (isGroup(node) ? 'box' : null);
|
|
569
|
+
if (kind) out.push({ loc: locOf(node).loc, label: labelOf(node, kind), kind });
|
|
570
|
+
}
|
|
571
|
+
return out;
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
/** All layers under a point of a clip's page, the smallest first (equal areas: the one drawn later first). */
|
|
575
|
+
const layersAt = (c, x, y, includeLocked) => {
|
|
576
|
+
const doc = c.win.document;
|
|
577
|
+
/* what is really at the point: a line's own stroke, not the box around it (a diagonal line's box covers the words
|
|
578
|
+
beside it, and as the smaller box it was taken for them) */
|
|
579
|
+
const under = seeingAll(doc, () => doc.elementsFromPoint(x, y));
|
|
580
|
+
const hits = [];
|
|
581
|
+
layersOf(doc).forEach((el, order) => {
|
|
582
|
+
const rect = mixedText(el) ? wordsRect(el) : el.getBoundingClientRect();
|
|
583
|
+
if (rect.width < 1 || rect.height < 1) return;
|
|
584
|
+
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) return;
|
|
585
|
+
if (!under.some((u) => el === u || el.contains(u))) return;
|
|
586
|
+
const frame = frameOf(el, rect);
|
|
587
|
+
if (frame.r && !inFrame(frame, x, y)) return;
|
|
588
|
+
const kind = kindOf(el);
|
|
589
|
+
if (!kind || invisible(el)) return;
|
|
590
|
+
if (!includeLocked && lockedOf(c, el)) return;
|
|
591
|
+
hits.push({ el, kind, rect, frame, area: frame.w * frame.h, order });
|
|
592
|
+
});
|
|
593
|
+
return hits.sort((a, b) => a.area - b.area || b.order - a.order);
|
|
594
|
+
};
|
|
595
|
+
/**
|
|
596
|
+
* A box that fills its whole page is the page's backdrop: a click there takes the clip
|
|
597
|
+
* (moving the picture), not the box (which would only move what is inside the page). It is selected in Layers, or
|
|
598
|
+
* with Alt, and once in hand a click on it keeps it.
|
|
599
|
+
*/
|
|
600
|
+
const backdrop = (c, hit) => hit.kind === 'box' && !hit.frame.r
|
|
601
|
+
&& hit.rect.left <= 1 && hit.rect.top <= 1 && hit.rect.right >= c.win.innerWidth - 1 && hit.rect.bottom >= c.win.innerHeight - 1;
|
|
602
|
+
/** A layer that fills its whole page, whatever it is (a vignette's box, a grain's canvas, a letterbox's picture). */
|
|
603
|
+
const fillsPage = (c, hit) => !hit.frame.r
|
|
604
|
+
&& hit.rect.left <= 1 && hit.rect.top <= 1 && hit.rect.right >= c.win.innerWidth - 1 && hit.rect.bottom >= c.win.innerHeight - 1;
|
|
605
|
+
/**
|
|
606
|
+
* The page a point of the picture is about. The top page under it, unless all it has there fills the page (a
|
|
607
|
+
* letterbox, a vignette, grain laid over the film) while a page under it has a layer of its own there: then that
|
|
608
|
+
* one, so a click on a title under a full-frame overlay takes the title. A layer in hand keeps its own page.
|
|
609
|
+
*/
|
|
610
|
+
const pageAt = (x, y, includeLocked, held) => {
|
|
611
|
+
const inside = (c) => { const p = matrixOf(c).inverse().transformPoint(new DOMPoint(x, y)); return p.x >= 0 && p.y >= 0 && p.x <= c.w && p.y <= c.h ? p : null; };
|
|
612
|
+
if (held) {
|
|
613
|
+
const own = shown().find((c) => c.kind === 'page' && c.win && c.win.document === held.ownerDocument && inside(c));
|
|
614
|
+
if (own) return own;
|
|
615
|
+
}
|
|
616
|
+
const top = clipAt(x, y);
|
|
617
|
+
if (!top || top.kind !== 'page' || !top.win) return top;
|
|
618
|
+
for (const c of shown()) {
|
|
619
|
+
const p = inside(c);
|
|
620
|
+
if (!p) continue;
|
|
621
|
+
if (c.kind !== 'page' || !c.win) break;
|
|
622
|
+
if (layersAt(c, p.x, p.y, includeLocked).some((h) => !fillsPage(c, h))) return c;
|
|
623
|
+
}
|
|
624
|
+
return top;
|
|
625
|
+
};
|
|
626
|
+
/** A layer inside a group is the group until the group is entered (a layer of it is in hand). */
|
|
627
|
+
const liftToGroup = (hit, current) => {
|
|
628
|
+
const groups = [];
|
|
629
|
+
for (let n = hit.el.parentElement; n && n !== hit.el.ownerDocument.body; n = n.parentElement) if (isGroup(n)) groups.unshift(n);
|
|
630
|
+
for (const g of groups) {
|
|
631
|
+
if (current && current !== g && g.contains(current)) continue;
|
|
632
|
+
return { el: g, kind: 'box' };
|
|
633
|
+
}
|
|
634
|
+
return hit;
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* The picture a layer draws, when it is one a file can be put in place of: an <img> (what it shows now, the file an
|
|
639
|
+
* override's `content` puts there before its own), or a box painted with one image (`bg`). A box that holds one
|
|
640
|
+
* <img> filling most of it (a frame around a photo, what a click on it takes first) has that picture: `at` / `n`
|
|
641
|
+
* name the <img>, where the file goes. `changed`: the clip has put another file there.
|
|
642
|
+
*/
|
|
643
|
+
const pictureOf = (c, el) => {
|
|
644
|
+
const url = (v) => /^url\("?([^"]*)"?\)$/.exec(v ?? '')?.[1] ?? null;
|
|
645
|
+
const changed = (target, key) => overrideOf(c, target)?.style?.[key] != null;
|
|
646
|
+
const imgOf = (img) => {
|
|
647
|
+
const src = url(css(img).content) ?? (img.currentSrc || img.src);
|
|
648
|
+
return src ? { kind: 'img', src, changed: changed(img, 'content') } : null;
|
|
649
|
+
};
|
|
650
|
+
if (el.localName.toLowerCase() === 'img') return imgOf(el);
|
|
651
|
+
const painted = url(css(el).backgroundImage);
|
|
652
|
+
if (painted) return { kind: 'bg', src: painted, changed: changed(el, 'backgroundImage') };
|
|
653
|
+
const imgs = [...el.querySelectorAll('img')].filter((img) => !img.closest('svg'));
|
|
654
|
+
if (imgs.length !== 1) return null;
|
|
655
|
+
const box = el.getBoundingClientRect();
|
|
656
|
+
const pic = imgs[0].getBoundingClientRect();
|
|
657
|
+
const w = Math.max(0, Math.min(box.right, pic.right) - Math.max(box.left, pic.left));
|
|
658
|
+
const h = Math.max(0, Math.min(box.bottom, pic.bottom) - Math.max(box.top, pic.top));
|
|
659
|
+
if (!(box.width * box.height > 0) || w * h < 0.6 * box.width * box.height) return null;
|
|
660
|
+
const inner = imgOf(imgs[0]);
|
|
661
|
+
return inner ? { ...inner, ...targetOf(imgs[0]) } : null;
|
|
662
|
+
};
|
|
663
|
+
/** What the editor gets of a layer: who it is, and where it is on the stage now. */
|
|
664
|
+
const describe = (c, el, kind = kindOf(el) ?? (isGroup(el) ? 'box' : null)) => {
|
|
665
|
+
if (!kind) return null;
|
|
666
|
+
const M = matrixOf(c);
|
|
667
|
+
const rect = rectOf(el);
|
|
668
|
+
const g = paintedOf(el);
|
|
669
|
+
const group = isGroup(el);
|
|
670
|
+
const local = group
|
|
671
|
+
? { cx: rect.left + rect.width / 2, cy: rect.top + rect.height / 2, w: rect.width, h: rect.height, r: 0 }
|
|
672
|
+
: measureFrame(el, g.s, totalTurn(el));
|
|
673
|
+
const words = kind === 'text' ? textOf(el) : '';
|
|
674
|
+
const picture = kind === 'image' || kind === 'box' ? pictureOf(c, el) : null;
|
|
675
|
+
const where = locOf(el);
|
|
676
|
+
const turn = clipTurn(M);
|
|
677
|
+
return {
|
|
678
|
+
handle: handleOf(el),
|
|
679
|
+
clip: c.id,
|
|
680
|
+
...where,
|
|
681
|
+
kind,
|
|
682
|
+
tag: el.localName.toLowerCase(),
|
|
683
|
+
...(similarOf(el) ? { similar: similarOf(el) } : {}),
|
|
684
|
+
label: labelOf(el, kind),
|
|
685
|
+
...(words ? { text: { value: words, shape: 'value' } } : {}),
|
|
686
|
+
...(picture ? { picture } : {}),
|
|
687
|
+
style: styleOf(el, kind),
|
|
688
|
+
parents: parentsOf(el),
|
|
689
|
+
override: overrideOf(c, el),
|
|
690
|
+
group,
|
|
691
|
+
locked: lockedOf(c, el),
|
|
692
|
+
svg: el.namespaceURI === SVG_NS,
|
|
693
|
+
rect: mapRect(M, rect),
|
|
694
|
+
frame: mapFrame(M, local),
|
|
695
|
+
geo: {
|
|
696
|
+
g,
|
|
697
|
+
/* stage px per override px */
|
|
698
|
+
k: localScale(el) * clipScale(M),
|
|
699
|
+
parentR: (el.parentElement ? totalTurn(el.parentElement) : 0) + turn,
|
|
700
|
+
around: totalTurn(el) - ownTurn(el) + turn,
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
};
|
|
704
|
+
/** Just where a hovered layer is (asked once a frame). */
|
|
705
|
+
const outline = (c, hit) => {
|
|
706
|
+
const M = matrixOf(c);
|
|
707
|
+
const turned = hit.frame.r || clipTurn(M);
|
|
708
|
+
return { handle: handleOf(hit.el), rect: mapRect(M, hit.rect), ...(turned ? { frame: mapFrame(M, hit.frame) } : {}) };
|
|
709
|
+
};
|
|
710
|
+
/** A layer by its handle, else by its clip and its selector (`at` and `n`, or a `loc`: `at`, or `at#n`). */
|
|
711
|
+
const layerOf = (ref) => {
|
|
712
|
+
let el = nodeOf(ref.handle);
|
|
713
|
+
let c = el ? clipOfNode(el) : null;
|
|
714
|
+
if (!el && ref.clip) {
|
|
715
|
+
c = clipById(ref.clip);
|
|
716
|
+
const doc = c?.win?.document;
|
|
717
|
+
const loc = ref.loc && /^(.*)#(\d+)$/.exec(ref.loc);
|
|
718
|
+
if (doc && ref.at) el = matches(doc, { at: ref.at, n: ref.n })[0] ?? null;
|
|
719
|
+
else if (doc && ref.loc) el = matches(doc, { at: ref.loc })[0] ?? (loc ? matches(doc, { at: loc[1], n: Number(loc[2]) })[0] : null) ?? null;
|
|
720
|
+
}
|
|
721
|
+
return el && c ? { el, c } : null;
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
/* ── changes shown before they are kept: the clip's overrides with the drafts merged, as its preview ── */
|
|
725
|
+
const drafts = new Map();
|
|
726
|
+
const sameTarget = (o, t) => o.at === t.at && (o.n ?? null) === (t.n ?? null);
|
|
727
|
+
const show = (c) => {
|
|
728
|
+
const d = drafts.get(c.id);
|
|
729
|
+
if (!d) return;
|
|
730
|
+
const list = (c.overrides ?? []).map((o) => ({ ...o, ...(o.style ? { style: { ...o.style } } : {}) }));
|
|
731
|
+
for (const { target, patch } of d.items.values()) {
|
|
732
|
+
/* null: the override taken away */
|
|
733
|
+
if (patch === null) { const i = list.findIndex((x) => sameTarget(x, target)); if (i >= 0) list.splice(i, 1); continue; }
|
|
734
|
+
let o = list.find((x) => sameTarget(x, target));
|
|
735
|
+
if (!o) { o = { at: target.at, ...(target.n ? { n: target.n } : {}) }; list.push(o); }
|
|
736
|
+
if (patch.t) o.t = patch.t;
|
|
737
|
+
if ('s' in patch) o.s = patch.s;
|
|
738
|
+
if ('r' in patch) o.r = patch.r;
|
|
739
|
+
if (patch.style) {
|
|
740
|
+
const style = { ...(o.style ?? {}) };
|
|
741
|
+
for (const [k, v] of Object.entries(patch.style)) { if (v == null || v === '') delete style[k]; else style[k] = v; }
|
|
742
|
+
o.style = style;
|
|
743
|
+
}
|
|
744
|
+
if ('text' in patch) { if (patch.text == null) delete o.text; else o.text = patch.text; }
|
|
745
|
+
}
|
|
746
|
+
editor()?.preview(c.id, list);
|
|
747
|
+
};
|
|
748
|
+
const draftKey = (target) => `${target.at}\u0000${target.n ?? ''}`;
|
|
749
|
+
/** Show a change of a layer (`patch` null: its override taken away); the blocks written inside it stay put. */
|
|
750
|
+
const draft = (c, target, patch) => {
|
|
751
|
+
let d = drafts.get(c.id);
|
|
752
|
+
if (!d) { d = { base: JSON.stringify(c.overrides ?? []), items: new Map() }; drafts.set(c.id, d); }
|
|
753
|
+
const el = c.win ? matches(c.win.document, target)[0] : null;
|
|
754
|
+
const blocks = el && mixedText(el) ? blocksOf(el).map(blockNow) : null;
|
|
755
|
+
const g0 = blocks ? paintedOf(el) : null;
|
|
756
|
+
const key = draftKey(target);
|
|
757
|
+
const prev = d.items.get(key)?.patch ?? {};
|
|
758
|
+
d.items.set(key, { target, patch: patch === null ? null : { ...prev, ...patch, ...(prev.style || patch.style ? { style: { ...(prev.style ?? {}), ...(patch.style ?? {}) } } : {}) } });
|
|
759
|
+
show(c);
|
|
760
|
+
if (blocks) holdBlocks(c, blocks, g0, paintedOf(el));
|
|
761
|
+
};
|
|
762
|
+
/* what a block keeps when the line it is written in changes: its look (the type it takes from the line) and its place */
|
|
763
|
+
const INHERITED = ['color', 'WebkitTextFillColor', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'letterSpacing', 'lineHeight', 'wordSpacing', 'textTransform', 'textAlign', 'textShadow', 'fontVariant'];
|
|
764
|
+
const blockNow = (b) => {
|
|
765
|
+
const cs = css(b);
|
|
766
|
+
const turn = totalTurn(b);
|
|
767
|
+
const f = measureFrame(b, paintedOf(b).s, turn);
|
|
768
|
+
const box = b.getBoundingClientRect();
|
|
769
|
+
/* held by the side its words start from (a block's width follows the line's, its words do not) */
|
|
770
|
+
const align = cs.textAlign;
|
|
771
|
+
const ax = /center/.test(align) ? 0.5 : /right|end/.test(align) ? 1 : 0;
|
|
772
|
+
return {
|
|
773
|
+
b, turn, ax, look: Object.fromEntries(INHERITED.map((k) => [k, cs[k]])),
|
|
774
|
+
box: { left: box.left, top: box.top, width: box.width, height: box.height },
|
|
775
|
+
frame: { w: f.w, h: f.h, ow: b.offsetWidth, oh: b.offsetHeight },
|
|
776
|
+
};
|
|
777
|
+
};
|
|
778
|
+
/**
|
|
779
|
+
* The blocks inside a line put back as they were: the type they took from it written on them, then their move, scale
|
|
780
|
+
* and turn undone around where they were (their own overrides, drafted: written with the line's edit, see riders).
|
|
781
|
+
*/
|
|
782
|
+
const holdBlocks = (c, before, g0, g1) => {
|
|
783
|
+
/* the line's own scale and turn, undone on each block (exact: from its overrides, not measured) */
|
|
784
|
+
const ks = [g1.s[0] ? g0.s[0] / g1.s[0] : 1, g1.s[1] ? g0.s[1] / g1.s[1] : 1];
|
|
785
|
+
const dr = g0.r - g1.r;
|
|
786
|
+
for (const was of before) {
|
|
787
|
+
const { b } = was;
|
|
788
|
+
const own = overrideOf(c, b)?.style ?? {};
|
|
789
|
+
const cs = css(b);
|
|
790
|
+
const look = {};
|
|
791
|
+
for (const k of INHERITED) if (!(k in own) && cs[k] !== was.look[k]) look[k] = was.look[k];
|
|
792
|
+
if (Object.keys(look).length) draft(c, targetOf(b), { style: look });
|
|
793
|
+
const now = blockNow(b);
|
|
794
|
+
const g = paintedOf(b);
|
|
795
|
+
/* its anchor (the start of its words, its top) where it was, with its box as it will be: its laid-out size at
|
|
796
|
+
the scale and turn it had */
|
|
797
|
+
const rad = (was.turn * Math.PI) / 180;
|
|
798
|
+
const fw = was.frame.ow > 0 ? (was.frame.w * b.offsetWidth) / was.frame.ow : 0;
|
|
799
|
+
const fh = was.frame.oh > 0 ? (was.frame.h * b.offsetHeight) / was.frame.oh : 0;
|
|
800
|
+
const flat = Math.abs(was.turn % 360) < 0.01;
|
|
801
|
+
const w = flat ? (was.box.width * b.offsetWidth) / (was.frame.ow || 1) : Math.abs(fw * Math.cos(rad)) + Math.abs(fh * Math.sin(rad));
|
|
802
|
+
const h = flat ? (was.box.height * b.offsetHeight) / (was.frame.oh || 1) : Math.abs(fw * Math.sin(rad)) + Math.abs(fh * Math.cos(rad));
|
|
803
|
+
const center = { x: was.box.left + was.ax * was.box.width + (0.5 - was.ax) * w, y: was.box.top + h / 2 };
|
|
804
|
+
const moved = Math.hypot(now.box.left + now.box.width / 2 - center.x, now.box.top + now.box.height / 2 - center.y);
|
|
805
|
+
if (Math.abs(ks[0] - 1) < 1e-4 && Math.abs(ks[1] - 1) < 1e-4 && Math.abs(dr) < 0.01 && moved < 0.05) continue;
|
|
806
|
+
const round = (n) => Math.round(n * 10000) / 10000;
|
|
807
|
+
paintAround(b, c, { t: g.t, s: [round(g.s[0] * ks[0]), round(g.s[1] * ks[1])], r: Math.round((g.r + dr) * 100) / 100 }, center);
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
const release = (id) => {
|
|
811
|
+
if (!drafts.delete(id)) return;
|
|
812
|
+
editor()?.preview(id, null);
|
|
813
|
+
};
|
|
814
|
+
/* the film was updated: a clip whose kept overrides changed shows them, not the drafts made against the old ones */
|
|
815
|
+
const updated = () => {
|
|
816
|
+
for (const [id, d] of drafts) {
|
|
817
|
+
const c = clipById(id);
|
|
818
|
+
if (!c || JSON.stringify(c.overrides ?? []) !== d.base) release(id);
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const paint = (el, c, g) => draft(c, targetOf(el), { t: [g.t[0], g.t[1]], s: g.s[0] === g.s[1] ? g.s[0] : [g.s[0], g.s[1]], r: g.r });
|
|
823
|
+
/**
|
|
824
|
+
* Paint an override and shift its offset until the layer's center sits at `target` (page px): CSS scales and turns
|
|
825
|
+
* about the box before the page's own transform, so a layer the page moved (or an SVG shape) would otherwise not
|
|
826
|
+
* scale or turn in place. The offset is outermost, a pure shift: paint, measure, correct, two or three times.
|
|
827
|
+
*/
|
|
828
|
+
const paintAround = (el, c, g, target) => {
|
|
829
|
+
/* the center moves with the offset as an affine map (whatever turns and scales the parents): measured at the offset
|
|
830
|
+
and one px along each axis, then solved for the offset that puts it at `target` */
|
|
831
|
+
const centerAt = (t) => {
|
|
832
|
+
paint(el, c, { ...g, t });
|
|
833
|
+
const box = mixedText(el) ? wordsRect(el) : el.getBoundingClientRect();
|
|
834
|
+
return [box.left + box.width / 2, box.top + box.height / 2];
|
|
835
|
+
};
|
|
836
|
+
const t0 = [g.t[0], g.t[1]];
|
|
837
|
+
const p0 = centerAt(t0);
|
|
838
|
+
let t = t0;
|
|
839
|
+
if (Math.abs(target.x - p0[0]) >= 0.05 || Math.abs(target.y - p0[1]) >= 0.05) {
|
|
840
|
+
const px = centerAt([t0[0] + 1, t0[1]]);
|
|
841
|
+
const py = centerAt([t0[0], t0[1] + 1]);
|
|
842
|
+
const a = px[0] - p0[0]; const b = py[0] - p0[0]; const cy = px[1] - p0[1]; const d = py[1] - p0[1];
|
|
843
|
+
const det = a * d - b * cy;
|
|
844
|
+
const ex = target.x - p0[0]; const ey = target.y - p0[1];
|
|
845
|
+
if (Math.abs(det) > 1e-9) t = [t0[0] + (d * ex - b * ey) / det, t0[1] + (a * ey - cy * ex) / det];
|
|
846
|
+
}
|
|
847
|
+
t = [Math.round(t[0] * 10) / 10, Math.round(t[1] * 10) / 10];
|
|
848
|
+
paint(el, c, { ...g, t });
|
|
849
|
+
return { ...g, t };
|
|
850
|
+
};
|
|
851
|
+
/** Where a frame's center goes when it takes a new size with `pivot` (a corner, an edge or the center) held. */
|
|
852
|
+
const pivotCenter = (f, next, pivot) => {
|
|
853
|
+
const rad = (f.r * Math.PI) / 180;
|
|
854
|
+
const ux = { x: Math.cos(rad), y: Math.sin(rad) };
|
|
855
|
+
const uy = { x: -Math.sin(rad), y: Math.cos(rad) };
|
|
856
|
+
const ax = f.cx + (ux.x * pivot.hx * f.w) / 2 + (uy.x * pivot.hy * f.h) / 2;
|
|
857
|
+
const ay = f.cy + (ux.y * pivot.hx * f.w) / 2 + (uy.y * pivot.hy * f.h) / 2;
|
|
858
|
+
return { x: ax - (ux.x * pivot.hx * next.w) / 2 - (uy.x * pivot.hy * next.h) / 2, y: ay - (ux.y * pivot.hx * next.w) / 2 - (uy.y * pivot.hy * next.h) / 2 };
|
|
859
|
+
};
|
|
860
|
+
const pair = (s) => (Array.isArray(s) ? [s[0] ?? 1, s[1] ?? 1] : typeof s === 'number' && s > 0 ? [s, s] : [1, 1]);
|
|
861
|
+
const placed = (el, c) => {
|
|
862
|
+
const M = matrixOf(c);
|
|
863
|
+
const g = paintedOf(el);
|
|
864
|
+
return { rect: mapRect(M, rectOf(el)), frame: mapFrame(M, measureFrame(el, g.s, totalTurn(el))) };
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
/* ── typing a line in place ── */
|
|
868
|
+
let editing = null;
|
|
869
|
+
/**
|
|
870
|
+
* The box typed in when the line cannot be typed in itself: an SVG text (no contentEditable there), or a line with
|
|
871
|
+
* inline parts (only its own words are its text). Fixed in the page over the line, in its type, the line hidden.
|
|
872
|
+
*/
|
|
873
|
+
const openOverlay = (el) => {
|
|
874
|
+
const doc = el.ownerDocument;
|
|
875
|
+
const rect = el.getBoundingClientRect();
|
|
876
|
+
const cs = css(el);
|
|
877
|
+
const svg = isSvgText(el);
|
|
878
|
+
let scale = 1;
|
|
879
|
+
if (svg) {
|
|
880
|
+
try {
|
|
881
|
+
const bb = el.getBBox?.();
|
|
882
|
+
if (bb && bb.height > 0 && rect.height > 0) scale = rect.height / bb.height;
|
|
883
|
+
else { const ctm = el.getScreenCTM?.(); if (ctm) scale = Math.hypot(ctm.a, ctm.b) || 1; }
|
|
884
|
+
} catch { scale = 1; }
|
|
885
|
+
}
|
|
886
|
+
const round = (n) => Math.round(n * 100) / 100;
|
|
887
|
+
const size = pxNumber(cs.fontSize);
|
|
888
|
+
const spacing = pxNumber(cs.letterSpacing);
|
|
889
|
+
const fill = cs.fill.trim();
|
|
890
|
+
const color = svg ? (/^url\(/i.test(fill) || fill === 'none' || !fill ? '#ffffff' : fill) : cs.color;
|
|
891
|
+
const align = svg ? (cs.textAnchor === 'middle' ? 'center' : cs.textAnchor === 'end' ? 'right' : 'left') : cs.textAlign;
|
|
892
|
+
const box = doc.createElement('div');
|
|
893
|
+
box.setAttribute(EDITOR_ATTR, '');
|
|
894
|
+
box.textContent = textOf(el);
|
|
895
|
+
const style = {
|
|
896
|
+
position: 'fixed', left: `${round(rect.left)}px`, top: `${round(rect.top)}px`,
|
|
897
|
+
'min-width': `${round(Math.max(1, rect.width))}px`, 'min-height': `${round(Math.max(1, rect.height))}px`,
|
|
898
|
+
...(svg ? { height: `${round(Math.max(1, rect.height))}px`, 'line-height': `${round(Math.max(1, rect.height))}px` } : { 'line-height': cs.lineHeight }),
|
|
899
|
+
margin: '0', padding: '0', border: '0', background: 'transparent', 'box-sizing': 'content-box',
|
|
900
|
+
'white-space': svg ? 'pre' : 'pre-wrap', 'text-align': align,
|
|
901
|
+
'font-family': cs.fontFamily, 'font-weight': cs.fontWeight, 'font-style': cs.fontStyle || 'normal',
|
|
902
|
+
'font-size': size != null ? `${round(size * scale)}px` : cs.fontSize,
|
|
903
|
+
...(spacing != null ? { 'letter-spacing': `${round(spacing * scale)}px` } : {}),
|
|
904
|
+
...(cs.textTransform && cs.textTransform !== 'none' ? { 'text-transform': cs.textTransform } : {}),
|
|
905
|
+
color, 'z-index': '2147483647',
|
|
906
|
+
};
|
|
907
|
+
for (const [name, value] of Object.entries(style)) box.style.setProperty(name, value, 'important');
|
|
908
|
+
(doc.body ?? doc.documentElement).appendChild(box);
|
|
909
|
+
return box;
|
|
910
|
+
};
|
|
911
|
+
const finishText = (commit) => {
|
|
912
|
+
const edit = editing;
|
|
913
|
+
if (!edit) return;
|
|
914
|
+
editing = null;
|
|
915
|
+
edit.cleanup();
|
|
916
|
+
const { el, host, c } = edit;
|
|
917
|
+
const value = edit.overlay ? collapse(host.innerText ?? host.textContent ?? '') : stageText(host);
|
|
918
|
+
if (edit.overlay) host.remove();
|
|
919
|
+
const put = (name, v) => { if (v == null) el.removeAttribute(name); else el.setAttribute(name, v); };
|
|
920
|
+
put('contenteditable', edit.attrs.contentEditable);
|
|
921
|
+
put('style', edit.attrs.style);
|
|
922
|
+
put('spellcheck', edit.attrs.spellcheck);
|
|
923
|
+
for (const [b, style] of edit.blocks) { if (style == null) b.removeAttribute('style'); else b.setAttribute('style', style); }
|
|
924
|
+
el.replaceChildren(...edit.children);
|
|
925
|
+
edit.data.forEach((text, n) => { n.data = text; });
|
|
926
|
+
el.ownerDocument.getSelection()?.removeAllRanges();
|
|
927
|
+
const kept = commit && value && value !== edit.before;
|
|
928
|
+
/* the new words stay on screen until the edit is kept (or refused: the editor releases the clip), with what held
|
|
929
|
+
the others in place as they were typed (kept with them) */
|
|
930
|
+
const hold = kept && edit.hold && Object.keys(edit.hold).length ? edit.hold : null;
|
|
931
|
+
if (kept) draft(c, edit.target, { text: value, ...(hold ? { style: hold } : {}) });
|
|
932
|
+
post({ type: 'stage-event', event: 'text', handle: edit.handle, commit: Boolean(kept), ...(kept ? { value, before: edit.before, ...(hold ? { hold } : {}) } : {}) });
|
|
933
|
+
};
|
|
934
|
+
/**
|
|
935
|
+
* Only `el` takes the pointer in its document until the returned function is called. While a line is typed in, a
|
|
936
|
+
* press on its words moves the caret or selects them, though the page draws something over it there (a number still
|
|
937
|
+
* to come in, at opacity 0; a grain) or another clip lies on top; a press anywhere else blurs it, which keeps it.
|
|
938
|
+
*/
|
|
939
|
+
const TYPING_ATTR = 'data-openfilm-typing';
|
|
940
|
+
const onlyHit = (el) => {
|
|
941
|
+
const doc = el.ownerDocument;
|
|
942
|
+
const style = doc.createElement('style');
|
|
943
|
+
style.textContent = `* { pointer-events: none !important; } [${TYPING_ATTR}], [${TYPING_ATTR}] * { pointer-events: auto !important; }`;
|
|
944
|
+
el.setAttribute(TYPING_ATTR, '');
|
|
945
|
+
(doc.head ?? doc.documentElement).append(style);
|
|
946
|
+
return () => { style.remove(); el.removeAttribute(TYPING_ATTR); };
|
|
947
|
+
};
|
|
948
|
+
const startText = ({ handle }) => {
|
|
949
|
+
const el = nodeOf(handle);
|
|
950
|
+
const c = el && clipOfNode(el);
|
|
951
|
+
if (!el || !c || editing) return false;
|
|
952
|
+
const doc = el.ownerDocument;
|
|
953
|
+
const data = new Map();
|
|
954
|
+
const walker = doc.createTreeWalker(el, 4);
|
|
955
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) data.set(n, n.data);
|
|
956
|
+
const attrs = { contentEditable: el.getAttribute('contenteditable'), style: el.getAttribute('style'), spellcheck: el.getAttribute('spellcheck') };
|
|
957
|
+
/* typed in place, lines and all, where it can be: the line keeps its own width and wraps as it does */
|
|
958
|
+
const overlay = isSvgText(el) || !onlyBreaks(el);
|
|
959
|
+
/* typed in place, the words beside it and under it stay where they are as it grows (holdSearch), its box hugging
|
|
960
|
+
them: worked out from the page as it is now, and shown on it as each word is typed */
|
|
961
|
+
const search = !overlay && el instanceof c.win.HTMLElement ? holdSearch(c.win, el, keptHold(c, targetOf(el)).unheld) : null;
|
|
962
|
+
const kebab = (k) => k.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase());
|
|
963
|
+
const inlineWas = new Map(HOLD_KEYS.map((k) => [k, [el.style.getPropertyValue(kebab(k)), el.style.getPropertyPriority(kebab(k))]]));
|
|
964
|
+
const holdInline = (extra) => {
|
|
965
|
+
for (const k of HOLD_KEYS) {
|
|
966
|
+
const prop = kebab(k);
|
|
967
|
+
if (!(k in extra)) { const [v, pr] = inlineWas.get(k); if (v) el.style.setProperty(prop, v, pr); else el.style.removeProperty(prop); }
|
|
968
|
+
else if (extra[k] == null) el.style.removeProperty(prop);
|
|
969
|
+
else el.style.setProperty(prop, extra[k], 'important');
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
/* the editor shows the words as they are typed (the inspector's box with them); they are kept at Enter or blur */
|
|
973
|
+
const onInput = () => {
|
|
974
|
+
if (!editing) return;
|
|
975
|
+
if (search) editing.hold = search(holdInline);
|
|
976
|
+
post({ type: 'stage-event', event: 'typing', handle, value: overlay ? collapse(host.innerText ?? host.textContent ?? '') : stageText(host) });
|
|
977
|
+
};
|
|
978
|
+
const host = overlay ? openOverlay(el) : el;
|
|
979
|
+
/* the line hidden under its overlay, the blocks in it still shown (they are not what is typed) */
|
|
980
|
+
const blocks = overlay ? blocksIn(el).map((b) => [b, b.getAttribute('style')]) : [];
|
|
981
|
+
if (overlay) el.style.setProperty('visibility', 'hidden', 'important');
|
|
982
|
+
for (const [b] of blocks) b.style.setProperty('visibility', 'visible', 'important');
|
|
983
|
+
try { host.contentEditable = 'plaintext-only'; } catch { host.contentEditable = 'true'; }
|
|
984
|
+
if (host.contentEditable !== 'plaintext-only') host.contentEditable = 'true';
|
|
985
|
+
host.spellcheck = false;
|
|
986
|
+
for (const [name, value] of [['user-select', 'text'], ['-webkit-user-select', 'text'], ['cursor', 'text'], ['outline', 'none'], ['pointer-events', 'auto'], ['caret-color', BLUE]]) {
|
|
987
|
+
host.style.setProperty(name, value, 'important');
|
|
988
|
+
}
|
|
989
|
+
/* Shift+Enter breaks the line (where the words can hold one); Enter keeps them, Esc gives them up */
|
|
990
|
+
const lines = !overlay;
|
|
991
|
+
const onKey = (e) => {
|
|
992
|
+
e.stopPropagation();
|
|
993
|
+
if (e.key === 'Escape') { e.preventDefault(); finishText(false); }
|
|
994
|
+
else if (e.key === 'Enter' && !(lines && e.shiftKey)) { e.preventDefault(); finishText(true); }
|
|
995
|
+
};
|
|
996
|
+
const onBlur = () => finishText(true);
|
|
997
|
+
host.addEventListener('keydown', onKey);
|
|
998
|
+
host.addEventListener('blur', onBlur);
|
|
999
|
+
host.addEventListener('input', onInput);
|
|
1000
|
+
editing = {
|
|
1001
|
+
el, host, c, handle, overlay, data, attrs, blocks, target: targetOf(el),
|
|
1002
|
+
children: [...el.childNodes],
|
|
1003
|
+
before: textOf(el),
|
|
1004
|
+
hold: null,
|
|
1005
|
+
cleanup: () => { host.removeEventListener('keydown', onKey); host.removeEventListener('blur', onBlur); host.removeEventListener('input', onInput); release(); },
|
|
1006
|
+
};
|
|
1007
|
+
/* the line in its page, and its page in the film */
|
|
1008
|
+
const hits = [onlyHit(host), onlyHit(c.el)];
|
|
1009
|
+
const release = () => { for (const off of hits) off(); };
|
|
1010
|
+
window.focus();
|
|
1011
|
+
c.win.focus();
|
|
1012
|
+
host.focus({ preventScroll: true });
|
|
1013
|
+
doc.getSelection()?.selectAllChildren(host);
|
|
1014
|
+
return true;
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
/* ── keys pressed while the picture has focus go to the editor (its own keys work there too) ── */
|
|
1018
|
+
const typing = (target) => {
|
|
1019
|
+
const el = target?.nodeType === 1 ? target : null;
|
|
1020
|
+
return Boolean(el && (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) || el.isContentEditable));
|
|
1021
|
+
};
|
|
1022
|
+
const EDITOR_KEYS = new Set([' ', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', '[', ']']);
|
|
1023
|
+
const onKey = (e) => {
|
|
1024
|
+
if (editing || typing(e.target)) return;
|
|
1025
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
1026
|
+
const k = e.key.toLowerCase();
|
|
1027
|
+
/* ⌘A is the editor's select-all, not the page's text selected */
|
|
1028
|
+
if ((EDITOR_KEYS.has(e.key) && !mod) || (mod && (k === 'z' || (k === 'a' && !e.shiftKey && !e.altKey) || (e.shiftKey && (k === 'h' || k === 'l'))))) e.preventDefault();
|
|
1029
|
+
post({ type: 'stage-event', event: 'key', key: e.key, code: e.code, repeat: e.repeat, metaKey: e.metaKey, ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, altKey: e.altKey });
|
|
1030
|
+
};
|
|
1031
|
+
/* by document: a page closed and opened again (the timeline does, far from the playhead) is a new one to hear */
|
|
1032
|
+
const listening = new WeakSet();
|
|
1033
|
+
const listen = (win) => {
|
|
1034
|
+
if (!win?.document || listening.has(win.document)) return;
|
|
1035
|
+
listening.add(win.document);
|
|
1036
|
+
win.addEventListener('keydown', onKey, true);
|
|
1037
|
+
};
|
|
1038
|
+
const listenAll = () => { listen(window); for (const c of clips()) if (c.win) listen(c.win); };
|
|
1039
|
+
listen(window);
|
|
1040
|
+
(async () => {
|
|
1041
|
+
while (!window.film) await new Promise((r) => setTimeout(r, 20));
|
|
1042
|
+
try { await window.film.ready; } catch { return; }
|
|
1043
|
+
listenAll();
|
|
1044
|
+
const h = hooks();
|
|
1045
|
+
/* an update may have added pages: their keys are heard too */
|
|
1046
|
+
if (h) { h.updated = () => { listenAll(); updated(); }; h.woke = listenAll; }
|
|
1047
|
+
})();
|
|
1048
|
+
|
|
1049
|
+
/* ── restacking ── */
|
|
1050
|
+
/** Hit tests that see every layer (a page may turn pointer events off), for as long as `fn` runs. */
|
|
1051
|
+
const seeingAll = (doc, fn) => {
|
|
1052
|
+
const probe = doc.createElement('style');
|
|
1053
|
+
probe.textContent = '* { pointer-events: auto !important; }';
|
|
1054
|
+
doc.head.append(probe);
|
|
1055
|
+
try { return fn(); } finally { probe.remove(); }
|
|
1056
|
+
};
|
|
1057
|
+
/**
|
|
1058
|
+
* Whether `upper` (or anything in it) is drawn over `lower` where their boxes overlap, as hit tests at a few points
|
|
1059
|
+
* there find them: null when they do not overlap or nothing of them is found there. The page's paint, not a model
|
|
1060
|
+
* of it (a transformed layer deep inside a group is drawn with the positioned ones, wherever its group is).
|
|
1061
|
+
*/
|
|
1062
|
+
const drawnOver = (upper, lower) => {
|
|
1063
|
+
const doc = upper.ownerDocument;
|
|
1064
|
+
const a = upper.getBoundingClientRect();
|
|
1065
|
+
const b = lower.getBoundingClientRect();
|
|
1066
|
+
const l = Math.max(a.left, b.left), t = Math.max(a.top, b.top);
|
|
1067
|
+
const r = Math.min(a.right, b.right), bt = Math.min(a.bottom, b.bottom);
|
|
1068
|
+
if (r - l < 1 || bt - t < 1) return null;
|
|
1069
|
+
let over = 0;
|
|
1070
|
+
let under = 0;
|
|
1071
|
+
for (const fx of [0.2, 0.5, 0.8]) for (const fy of [0.2, 0.5, 0.8]) {
|
|
1072
|
+
const hits = doc.elementsFromPoint(l + (r - l) * fx, t + (bt - t) * fy);
|
|
1073
|
+
const iu = hits.findIndex((h) => upper === h || upper.contains(h));
|
|
1074
|
+
const il = hits.findIndex((h) => lower === h || lower.contains(h));
|
|
1075
|
+
if (iu < 0 || il < 0) continue;
|
|
1076
|
+
if (iu < il) over += 1; else under += 1;
|
|
1077
|
+
}
|
|
1078
|
+
return over + under ? over > under : null;
|
|
1079
|
+
};
|
|
1080
|
+
const near = (p, q) => Math.abs(p.left - q.left) < 0.5 && Math.abs(p.top - q.top) < 0.5 && Math.abs(p.width - q.width) < 0.5 && Math.abs(p.height - q.height) < 0.5;
|
|
1081
|
+
/**
|
|
1082
|
+
* Restacking edits (override targets and styles) as they can be kept, or null. A position given to a layer the page
|
|
1083
|
+
* left static makes the offsets its CSS gave it count (top: 2em on a static block is nothing, on a relative one a
|
|
1084
|
+
* shift), so the edits are shown for a moment and every layer measured, then again with those offsets set back to
|
|
1085
|
+
* auto. `check`, asked while they are shown: whether they do what they were for.
|
|
1086
|
+
*/
|
|
1087
|
+
const steady = (c, edits, check = () => true) => {
|
|
1088
|
+
const seen = layersOf(c.win.document);
|
|
1089
|
+
const before = seen.map((o) => o.getBoundingClientRect());
|
|
1090
|
+
const had = drafts.get(c.id);
|
|
1091
|
+
const kept = had && new Map(had.items);
|
|
1092
|
+
const holds = (list) => {
|
|
1093
|
+
for (const e of list) draft(c, e.target, { style: e.style });
|
|
1094
|
+
const ok = seen.every((o, i) => near(o.getBoundingClientRect(), before[i])) && check();
|
|
1095
|
+
if (had) { had.items = new Map(kept); drafts.set(c.id, had); show(c); } else release(c.id);
|
|
1096
|
+
return ok;
|
|
1097
|
+
};
|
|
1098
|
+
if (holds(edits)) return edits;
|
|
1099
|
+
const inset = { top: 'auto', right: 'auto', bottom: 'auto', left: 'auto' };
|
|
1100
|
+
const pinned = edits.map((e) => (e.style.position ? { ...e, style: { ...e.style, ...inset } } : e));
|
|
1101
|
+
return pinned.some((e) => e.style.position) && holds(pinned) ? pinned : null;
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
/* ── a layer restyled: the others stay where they are; one layer's look never moves another ── */
|
|
1105
|
+
/** The style a hold sets (holdFor): kept apart from what the person set, so a new hold replaces the one before. */
|
|
1106
|
+
const HOLD_KEYS = ['marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'width'];
|
|
1107
|
+
const SIDES = ['Top', 'Right', 'Bottom', 'Left'];
|
|
1108
|
+
/**
|
|
1109
|
+
* The search for a hold, begun while the page shows `el` as it was: what the other layers' places are, the room `el`
|
|
1110
|
+
* takes in the page's flow, where its words sit. Returned: the search itself, given how to show `el` changed with a
|
|
1111
|
+
* hold's style on it (`show`, style keys to null take a kept one away). `unheld`: a hold kept before, taken away
|
|
1112
|
+
* first (it was for the change before).
|
|
1113
|
+
*
|
|
1114
|
+
* The layer takes its new size (its box grows with a larger font or more words); margins
|
|
1115
|
+
* give back to the page's flow the room it gained or lost, on the side the flow lays it out from, so what is beside
|
|
1116
|
+
* it, under it or lined up with its baseline stays put. Words in a block as wide as what holds it get a box that hugs
|
|
1117
|
+
* them, and stay where they were set in it (centered, at its end).
|
|
1118
|
+
*/
|
|
1119
|
+
const holdSearch = (win, el, unheld) => {
|
|
1120
|
+
const others = layersOf(el.ownerDocument).filter((o) => o !== el && !o.contains(el) && !el.contains(o));
|
|
1121
|
+
const before = others.map((o) => o.getBoundingClientRect());
|
|
1122
|
+
const marginsNow = () => {
|
|
1123
|
+
const cs = win.getComputedStyle(el);
|
|
1124
|
+
return Object.fromEntries(SIDES.map((side) => [side, Number.parseFloat(cs[`margin${side}`]) || 0]));
|
|
1125
|
+
};
|
|
1126
|
+
const was = marginsNow();
|
|
1127
|
+
const roomW = el.offsetWidth + was.Left + was.Right;
|
|
1128
|
+
const roomH = el.offsetHeight + was.Top + was.Bottom;
|
|
1129
|
+
const cs = win.getComputedStyle(el);
|
|
1130
|
+
const fit = Boolean(textOf(el)) && !isGroup(el) && /^(block|flow-root|list-item)$/.test(cs.display) ? { width: 'fit-content' } : {};
|
|
1131
|
+
const align = cs.textAlign;
|
|
1132
|
+
const anchorOf = () => (/center/.test(align) ? el.offsetLeft + el.offsetWidth / 2 : /right|end/.test(align) ? el.offsetLeft + el.offsetWidth : el.offsetLeft);
|
|
1133
|
+
const anchor0 = anchorOf();
|
|
1134
|
+
/* how far the others went, the furthest, and which way up or down */
|
|
1135
|
+
const moved = () => {
|
|
1136
|
+
let far = 0;
|
|
1137
|
+
let dy = 0;
|
|
1138
|
+
others.forEach((o, i) => {
|
|
1139
|
+
const r = o.getBoundingClientRect();
|
|
1140
|
+
const d = Math.max(Math.abs(r.left - before[i].left), Math.abs(r.top - before[i].top));
|
|
1141
|
+
if (d > far) { far = d; dy = r.top - before[i].top; }
|
|
1142
|
+
});
|
|
1143
|
+
return { far, dy };
|
|
1144
|
+
};
|
|
1145
|
+
return (show) => {
|
|
1146
|
+
const base = { ...unheld, ...fit };
|
|
1147
|
+
show(base);
|
|
1148
|
+
/* changed, with the page's own margins: the room it would take, more or less than it takes now */
|
|
1149
|
+
const margin = marginsNow();
|
|
1150
|
+
const shift = fit.width ? anchor0 - anchorOf() : 0;
|
|
1151
|
+
const dw = el.offsetWidth + margin.Left + margin.Right - roomW;
|
|
1152
|
+
const dh = el.offsetHeight + margin.Top + margin.Bottom - roomH;
|
|
1153
|
+
const hold = (give) => {
|
|
1154
|
+
const side = { ...give, Left: (give.Left ?? 0) - shift, Right: (give.Right ?? 0) + shift };
|
|
1155
|
+
return { ...base, ...Object.fromEntries(SIDES.filter((k) => side[k]).map((k) => [`margin${k}`, `${Math.round((margin[k] - side[k]) * 100) / 100}px`])) };
|
|
1156
|
+
};
|
|
1157
|
+
show(hold({}));
|
|
1158
|
+
const alone = moved().far;
|
|
1159
|
+
if (alone < 0.5) return hold({});
|
|
1160
|
+
let best = { hold: hold({}), far: alone };
|
|
1161
|
+
const tryGive = (give) => {
|
|
1162
|
+
const h = hold(give);
|
|
1163
|
+
show(h);
|
|
1164
|
+
const m = moved();
|
|
1165
|
+
if (m.far < best.far) best = { hold: h, far: m.far };
|
|
1166
|
+
return m;
|
|
1167
|
+
};
|
|
1168
|
+
/* laid out from the start, the middle or the end of its row and of its column */
|
|
1169
|
+
const across = [{ Right: dw }, { Left: dw / 2, Right: dw / 2 }, { Left: dw }];
|
|
1170
|
+
const down = [{ Bottom: dh }, { Top: dh / 2, Bottom: dh / 2 }, { Top: dh }];
|
|
1171
|
+
for (const x of across) {
|
|
1172
|
+
for (const y of down) {
|
|
1173
|
+
const give = { ...x, ...y };
|
|
1174
|
+
let m = tryGive(give);
|
|
1175
|
+
/* lined up by its baseline: the words sit higher or lower in the new box, the room above them gives that back */
|
|
1176
|
+
for (let i = 0; i < 2 && m.far >= 0.5 && Math.abs(m.dy) >= 0.5; i++) {
|
|
1177
|
+
give.Top = (give.Top ?? 0) + m.dy;
|
|
1178
|
+
give.Bottom = (give.Bottom ?? 0) - m.dy;
|
|
1179
|
+
m = tryGive(give);
|
|
1180
|
+
}
|
|
1181
|
+
if (best.far < 0.5) return best.hold;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
show(best.hold);
|
|
1185
|
+
return best.hold;
|
|
1186
|
+
};
|
|
1187
|
+
};
|
|
1188
|
+
/** The style keys of a hold kept for this layer, to null: taken away before a hold is worked out again. */
|
|
1189
|
+
const keptHold = (c, target) => {
|
|
1190
|
+
const kept = (c.overrides ?? []).find((o) => o.at === target.at && (o.n ?? null) === (target.n ?? null));
|
|
1191
|
+
return { style: kept?.style ?? {}, text: kept?.text, unheld: Object.fromEntries(HOLD_KEYS.filter((k) => k in (kept?.style ?? {})).map((k) => [k, null])) };
|
|
1192
|
+
};
|
|
1193
|
+
/**
|
|
1194
|
+
* What the layer at `target` needs besides `style` (and new words, `text`) so that no other layer of its page moves
|
|
1195
|
+
* (holdSearch): style for the same override; {} when it needs none, or the change is what is kept already (a preview
|
|
1196
|
+
* taken back: the hold kept with it stands). Measured against the page as shown without this change (the kept
|
|
1197
|
+
* overrides, and other changes not kept yet).
|
|
1198
|
+
*/
|
|
1199
|
+
const holdFor = (c, target, style, text) => {
|
|
1200
|
+
const doc = c.win?.document;
|
|
1201
|
+
/* a size or margin the person sets is meant to move what is around it */
|
|
1202
|
+
if (!doc || ['width', 'height', ...HOLD_KEYS, 'margin'].some((k) => k in style)) return {};
|
|
1203
|
+
const kept = keptHold(c, target);
|
|
1204
|
+
if (Object.entries(style).every(([k, v]) => k in kept.style && String(kept.style[k]) === String(v)) && (text == null || text === kept.text)) return {};
|
|
1205
|
+
const had = drafts.get(c.id);
|
|
1206
|
+
const items0 = had && new Map(had.items);
|
|
1207
|
+
const key = draftKey(target);
|
|
1208
|
+
const own = items0?.get(key)?.patch;
|
|
1209
|
+
const ownStyle = { ...(own?.style ?? {}) };
|
|
1210
|
+
for (const k of [...Object.keys(style), ...HOLD_KEYS]) delete ownStyle[k];
|
|
1211
|
+
const { text: _ownText, ...ownRest } = own ?? {};
|
|
1212
|
+
const showWith = (extra, changed) => {
|
|
1213
|
+
const items = new Map(items0 ?? []);
|
|
1214
|
+
const patch = { ...(changed || text == null ? own ?? {} : ownRest), style: { ...ownStyle, ...(changed ? style : {}), ...extra }, ...(changed && text != null ? { text } : {}) };
|
|
1215
|
+
items.set(key, { target, patch });
|
|
1216
|
+
drafts.set(c.id, { base: had?.base ?? JSON.stringify(c.overrides ?? []), items });
|
|
1217
|
+
show(c);
|
|
1218
|
+
};
|
|
1219
|
+
try {
|
|
1220
|
+
showWith({}, false);
|
|
1221
|
+
const el = matches(doc, target)[0];
|
|
1222
|
+
if (!el || !(el instanceof c.win.HTMLElement)) return {};
|
|
1223
|
+
return holdSearch(c.win, el, kept.unheld)((extra) => showWith(extra, true));
|
|
1224
|
+
} finally {
|
|
1225
|
+
if (had) { had.items = new Map(items0); drafts.set(c.id, had); show(c); } else release(c.id);
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
/* ── the Layers panel's tree ── */
|
|
1230
|
+
const ROWS = 800;
|
|
1231
|
+
/** Per element, whether the clip's kept overrides hide it (the panel's eye) or lock it: the last one to say counts. */
|
|
1232
|
+
const ownOverrides = (c) => {
|
|
1233
|
+
const out = new Map();
|
|
1234
|
+
for (const o of c.overrides ?? []) {
|
|
1235
|
+
const says = {
|
|
1236
|
+
...(o.style && 'visibility' in o.style ? { hidden: o.style.visibility === 'hidden' } : {}),
|
|
1237
|
+
...('lock' in o ? { locked: o.lock === true } : {}),
|
|
1238
|
+
};
|
|
1239
|
+
if (Object.keys(says).length) for (const el of matches(c.win.document, o)) out.set(el, { ...out.get(el), ...says });
|
|
1240
|
+
}
|
|
1241
|
+
return out;
|
|
1242
|
+
};
|
|
1243
|
+
const nameOf = (el) => {
|
|
1244
|
+
if (el.id) return `#${el.id}`;
|
|
1245
|
+
const cls = [...el.classList].find((k) => /^[A-Za-z_][\w-]*$/.test(k));
|
|
1246
|
+
return cls ? `${el.localName}.${cls}` : el.localName;
|
|
1247
|
+
};
|
|
1248
|
+
/**
|
|
1249
|
+
* A page's layers as rows: words, pictures and SVG drawings are rows without rows inside; an element around
|
|
1250
|
+
* several rows is a group row (its own paint, if any, makes it a box); one around a single row is no row of its
|
|
1251
|
+
* own (that row stands for it). `branch` is the handle of the child of the row's parent row that holds it: rows are
|
|
1252
|
+
* restacked by it. `off`: not drawn at this moment (faded out, hidden, no size).
|
|
1253
|
+
*/
|
|
1254
|
+
const rowsOf = (c) => {
|
|
1255
|
+
let count = 0;
|
|
1256
|
+
const doc = c.win.document;
|
|
1257
|
+
const kept = ownOverrides(c);
|
|
1258
|
+
const collect = (parentEl) => {
|
|
1259
|
+
const out = [];
|
|
1260
|
+
for (const ch of parentEl.children) {
|
|
1261
|
+
if (count >= ROWS) break;
|
|
1262
|
+
const tag = ch.localName.toLowerCase();
|
|
1263
|
+
if (SKIP.has(tag) || ch.hasAttribute(EDITOR_ATTR)) continue;
|
|
1264
|
+
const kind = kindOf(ch) ?? (isGroup(ch) ? 'box' : null);
|
|
1265
|
+
const leaf = tag === 'svg' || kind === 'text' || kind === 'image';
|
|
1266
|
+
const inner = leaf ? [] : collect(ch);
|
|
1267
|
+
if (!kind && !leaf) {
|
|
1268
|
+
if (!inner.length) continue;
|
|
1269
|
+
if (inner.length === 1) { out.push({ ...inner[0], branch: handleOf(ch), at: ch }); continue; }
|
|
1270
|
+
}
|
|
1271
|
+
count += 1;
|
|
1272
|
+
const own = kept.get(ch) ?? {};
|
|
1273
|
+
const r = ch.getBoundingClientRect();
|
|
1274
|
+
const where = locOf(ch);
|
|
1275
|
+
const named = kind ? labelOf(ch, kind) : tag;
|
|
1276
|
+
out.push({
|
|
1277
|
+
handle: handleOf(ch),
|
|
1278
|
+
branch: handleOf(ch),
|
|
1279
|
+
at: ch,
|
|
1280
|
+
loc: where.loc,
|
|
1281
|
+
...(where.instance ? { instance: where.instance } : {}),
|
|
1282
|
+
tag,
|
|
1283
|
+
kind: kind ?? (leaf ? 'shape' : 'group'),
|
|
1284
|
+
/* a layer with nothing better to go by than its tag goes by its id or class */
|
|
1285
|
+
label: named === tag ? nameOf(ch) : named,
|
|
1286
|
+
hidden: own.hidden === true,
|
|
1287
|
+
locked: own.locked === true,
|
|
1288
|
+
off: invisible(ch) || css(ch).visibility === 'hidden' || r.width < 1 || r.height < 1,
|
|
1289
|
+
children: inner,
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
/* the one drawn on top first; `at` is the element each row is restacked by, dropped once sorted */
|
|
1293
|
+
const drawn = siblingOrder(out.map((r) => r.at));
|
|
1294
|
+
return out.sort((a, b) => drawn.indexOf(b.at) - drawn.indexOf(a.at)).map(({ at, ...row }) => row);
|
|
1295
|
+
};
|
|
1296
|
+
return collect(doc.body);
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
/* ── the ops ── */
|
|
1300
|
+
const ops = {
|
|
1301
|
+
/** The layer under a point: the smallest (Alt: the next one under `cycleFrom`); groups stand for their layers. */
|
|
1302
|
+
hit({ x, y, cycleFrom, includeLocked, from, enter, detail }) {
|
|
1303
|
+
const c = pageAt(x, y, Boolean(includeLocked), nodeOf(cycleFrom) ?? nodeOf(from));
|
|
1304
|
+
if (!c || c.kind !== 'page' || !c.win) return null;
|
|
1305
|
+
const p = matrixOf(c).inverse().transformPoint(new DOMPoint(x, y));
|
|
1306
|
+
const all = layersAt(c, p.x, p.y, Boolean(includeLocked));
|
|
1307
|
+
let current = nodeOf(from);
|
|
1308
|
+
if (enter && current) current = current.firstElementChild;
|
|
1309
|
+
const cycle = nodeOf(cycleFrom);
|
|
1310
|
+
let hit;
|
|
1311
|
+
if (!cycle || all.length < 2) hit = all[0] && (cycle || all[0].el === current || !backdrop(c, all[0])) ? liftToGroup(all[0], current) : null;
|
|
1312
|
+
else hit = all[(all.findIndex((h) => h.el === cycle) + 1) % all.length] ?? all[0];
|
|
1313
|
+
if (!hit) return null;
|
|
1314
|
+
if (detail) return describe(c, hit.el, hit.kind);
|
|
1315
|
+
const rect = hit.rect ?? rectOf(hit.el);
|
|
1316
|
+
return outline(c, { el: hit.el, rect, frame: hit.frame ?? frameOf(hit.el, rect) });
|
|
1317
|
+
},
|
|
1318
|
+
/** A layer again, by handle, else by its clip and selector (after the page was drawn anew). */
|
|
1319
|
+
/** `any`: a layer that paints nothing itself (a Layers panel group) is taken as a box. */
|
|
1320
|
+
layer(ref) {
|
|
1321
|
+
const found = layerOf(ref);
|
|
1322
|
+
if (!found) return null;
|
|
1323
|
+
return describe(found.c, found.el, kindOf(found.el) ?? (isGroup(found.el) || ref.any ? 'box' : null));
|
|
1324
|
+
},
|
|
1325
|
+
layers({ refs }) { return refs.map((ref) => { const found = layerOf(ref); return found ? describe(found.c, found.el) : null; }); },
|
|
1326
|
+
/**
|
|
1327
|
+
* The outermost layers wholly inside a stage box (a marquee): not locked, not faded out, not under a pixel. `all`:
|
|
1328
|
+
* every one of them, those inside others too (the words in a box drawn to point at).
|
|
1329
|
+
*/
|
|
1330
|
+
box({ x, y, w, h, all }) {
|
|
1331
|
+
const out = [];
|
|
1332
|
+
for (const c of shown()) {
|
|
1333
|
+
if (c.kind !== 'page' || !c.win) continue;
|
|
1334
|
+
const M = matrixOf(c);
|
|
1335
|
+
const inside = layersOf(c.win.document).filter((el) => {
|
|
1336
|
+
if (!(kindOf(el) || isGroup(el))) return false;
|
|
1337
|
+
const r = mapRect(M, rectOf(el));
|
|
1338
|
+
if (r.width < 1 || r.height < 1) return false;
|
|
1339
|
+
if (r.left < x || r.top < y || r.left + r.width > x + w || r.top + r.height > y + h) return false;
|
|
1340
|
+
return !invisible(el) && !lockedOf(c, el);
|
|
1341
|
+
});
|
|
1342
|
+
for (const el of inside) if (all || !inside.some((other) => other !== el && other.contains(el))) out.push(describe(c, el));
|
|
1343
|
+
}
|
|
1344
|
+
return out.filter(Boolean);
|
|
1345
|
+
},
|
|
1346
|
+
/** What the snapping of a layer move can catch: the stage, and up to 80 other visible layers of its page. */
|
|
1347
|
+
snaps({ handle }) {
|
|
1348
|
+
const el = nodeOf(handle);
|
|
1349
|
+
const c = el && clipOfNode(el);
|
|
1350
|
+
const out = [{ x: 0, y: 0, w: document.body.offsetWidth, h: document.body.offsetHeight }];
|
|
1351
|
+
if (!el || !c) return out;
|
|
1352
|
+
const M = matrixOf(c);
|
|
1353
|
+
for (const other of layersOf(c.win.document)) {
|
|
1354
|
+
if (out.length > 80) break;
|
|
1355
|
+
if (other === el || other.contains(el) || el.contains(other)) continue;
|
|
1356
|
+
const r = other.getBoundingClientRect();
|
|
1357
|
+
if (r.width < 2 || r.height < 2 || invisible(other)) continue;
|
|
1358
|
+
const m = mapRect(M, r);
|
|
1359
|
+
out.push({ x: m.left, y: m.top, w: m.width, h: m.height });
|
|
1360
|
+
}
|
|
1361
|
+
return out;
|
|
1362
|
+
},
|
|
1363
|
+
/** The picture clips: whether each is on screen now, and its box on the stage. */
|
|
1364
|
+
clips() {
|
|
1365
|
+
const on = new Set(shown());
|
|
1366
|
+
return clips().filter((c) => c.el && c.kind !== 'sound').map((c) => {
|
|
1367
|
+
const r = mapRect(matrixOf(c), { left: 0, top: 0, width: c.w, height: c.h });
|
|
1368
|
+
return { id: c.id, visible: on.has(c), x: r.left, y: r.top, w: r.width, h: r.height };
|
|
1369
|
+
});
|
|
1370
|
+
},
|
|
1371
|
+
/** `pictures`: a video or a still only (a page is clicked by its layers; what is under its empty parts is found). */
|
|
1372
|
+
clipAt({ x, y, pictures }) { return clipAt(x, y, Boolean(pictures))?.id ?? null; },
|
|
1373
|
+
/** Show a clip placed in a box not kept yet (null: in its own again). */
|
|
1374
|
+
place({ clip, box }) { hooks()?.place(clip, box ?? null); return true; },
|
|
1375
|
+
/** Show a clip in CSS of its own not kept yet (null: its own again). */
|
|
1376
|
+
look({ clip, css }) { hooks()?.look?.(clip, typeof css === 'string' ? css : null); return true; },
|
|
1377
|
+
/** Show a layer's move / scale / turn; with `center` (stage px), its offset keeps its center there. */
|
|
1378
|
+
paint({ handle, g, center }) {
|
|
1379
|
+
const el = nodeOf(handle);
|
|
1380
|
+
const c = el && clipOfNode(el);
|
|
1381
|
+
if (!el || !c) return null;
|
|
1382
|
+
let out = g;
|
|
1383
|
+
if (center) {
|
|
1384
|
+
const p = matrixOf(c).inverse().transformPoint(new DOMPoint(center.x, center.y));
|
|
1385
|
+
out = paintAround(el, c, g, { x: p.x, y: p.y });
|
|
1386
|
+
} else paint(el, c, g);
|
|
1387
|
+
return { g: out, ...placed(el, c) };
|
|
1388
|
+
},
|
|
1389
|
+
paintMany({ items }) {
|
|
1390
|
+
return items.map(({ handle, g }) => {
|
|
1391
|
+
const el = nodeOf(handle);
|
|
1392
|
+
const c = el && clipOfNode(el);
|
|
1393
|
+
if (!el || !c) return null;
|
|
1394
|
+
paint(el, c, g);
|
|
1395
|
+
return placed(el, c);
|
|
1396
|
+
});
|
|
1397
|
+
},
|
|
1398
|
+
/** The inspector's live tweaks of a layer (named by clip and selector): style keys, words, geometry. */
|
|
1399
|
+
tweak({ clip, at, n, patch }) {
|
|
1400
|
+
const c = clipById(clip);
|
|
1401
|
+
if (!c || c.kind !== 'page') return false;
|
|
1402
|
+
const target = n ? { at, n } : { at };
|
|
1403
|
+
if (!patch.style && typeof patch.text !== 'string') { draft(c, target, patch); return true; }
|
|
1404
|
+
/* restyled or retyped, held in place (holdFor): the hold shown before gives way to this one */
|
|
1405
|
+
const style = patch.style ?? {};
|
|
1406
|
+
const hold = holdFor(c, target, style, typeof patch.text === 'string' ? patch.text : undefined);
|
|
1407
|
+
const shown = drafts.get(c.id)?.items.get(draftKey(target))?.patch.style;
|
|
1408
|
+
if (shown) for (const k of HOLD_KEYS) if (!(k in style)) delete shown[k];
|
|
1409
|
+
draft(c, target, { ...patch, style: { ...style, ...hold } });
|
|
1410
|
+
return true;
|
|
1411
|
+
},
|
|
1412
|
+
/**
|
|
1413
|
+
* What goes with an edit of a line that has blocks written inside it (`patch` null: its override taken away): the
|
|
1414
|
+
* edit shown, the overrides of the layers inside it that keep them where they were and as they looked, to be
|
|
1415
|
+
* written with it ([] for any other layer).
|
|
1416
|
+
*/
|
|
1417
|
+
riders({ clip, at, n, patch }) {
|
|
1418
|
+
const c = clipById(clip);
|
|
1419
|
+
const target = n ? { at, n } : { at };
|
|
1420
|
+
const el = c?.win ? matches(c.win.document, target)[0] : null;
|
|
1421
|
+
if (!c || !el || !mixedText(el)) return [];
|
|
1422
|
+
draft(c, target, patch ?? null);
|
|
1423
|
+
const out = [];
|
|
1424
|
+
for (const item of drafts.get(c.id)?.items.values() ?? []) {
|
|
1425
|
+
const inner = matches(c.win.document, item.target)[0];
|
|
1426
|
+
if (inner && inner !== el && el.contains(inner) && item.patch) out.push({ ...item.target, patch: item.patch });
|
|
1427
|
+
}
|
|
1428
|
+
return out;
|
|
1429
|
+
},
|
|
1430
|
+
/** What keeps the other layers in place when this style (and these words) is kept for the layer at `at` (holdFor). */
|
|
1431
|
+
hold({ clip, at, n, style, text }) {
|
|
1432
|
+
const c = clipById(clip);
|
|
1433
|
+
return c?.kind === 'page' ? holdFor(c, n ? { at, n } : { at }, style ?? {}, typeof text === 'string' ? text : undefined) : {};
|
|
1434
|
+
},
|
|
1435
|
+
/**
|
|
1436
|
+
* The inspector's scale / turn of a layer about a pivot (a corner of its box, or its center): the offset that keeps
|
|
1437
|
+
* that point in place, shown.
|
|
1438
|
+
*/
|
|
1439
|
+
pivot({ clip, at, n, base, patch, pivot }) {
|
|
1440
|
+
const c = clipById(clip);
|
|
1441
|
+
const el = c?.win ? matches(c.win.document, { at, n })[0] : null;
|
|
1442
|
+
if (!el || !c) return null;
|
|
1443
|
+
const b = { t: base.t ?? [0, 0], s: pair(base.s), r: base.r ?? 0 };
|
|
1444
|
+
paint(el, c, b);
|
|
1445
|
+
const f0 = measureFrame(el, b.s, totalTurn(el));
|
|
1446
|
+
const next = pair(patch.s ?? base.s);
|
|
1447
|
+
const center = pivotCenter(f0, { w: (f0.w * next[0]) / b.s[0], h: (f0.h * next[1]) / b.s[1] }, pivot);
|
|
1448
|
+
return paintAround(el, c, { t: patch.t ?? b.t, s: next, r: patch.r ?? b.r }, center).t;
|
|
1449
|
+
},
|
|
1450
|
+
/**
|
|
1451
|
+
* The person's changes inside pages whose element the page no longer has: per page clip with changes (or only
|
|
1452
|
+
* `clip`), the selectors (`at`) of the lost ones. A change is lost when its page is drawn, no frame drawn so far
|
|
1453
|
+
* found its element and the page does not have it now either (an element a page makes only at some moments is
|
|
1454
|
+
* not lost once a frame found it).
|
|
1455
|
+
*/
|
|
1456
|
+
lostOverrides({ clip } = {}) {
|
|
1457
|
+
return clips()
|
|
1458
|
+
.filter((c) => c.kind === 'page' && c.win && c.drawn && c.overrides?.length && (clip == null || c.id === clip))
|
|
1459
|
+
.map((c) => ({
|
|
1460
|
+
clip: c.id,
|
|
1461
|
+
lost: c.overrides.filter((o, i) => o?.at && !c.found?.[i] && !matches(c.win.document, o).length).map((o) => o.at),
|
|
1462
|
+
}));
|
|
1463
|
+
},
|
|
1464
|
+
/** Stop showing what was not kept on a clip (a refused edit, a cancelled drag). */
|
|
1465
|
+
release({ clip }) { release(clip); return true; },
|
|
1466
|
+
/**
|
|
1467
|
+
* Bring a layer to the front of what it overlaps, or send it to the back of it — whatever part
|
|
1468
|
+
* of the page the other layer is in. A z-index orders only siblings, so for each layer it overlaps the order is
|
|
1469
|
+
* set where the two meet: on the two branches under their nearest common ancestor, the branch it is in is given a
|
|
1470
|
+
* z-index past the other's (with position: relative where it has none, which moves nothing). Sent back, a common
|
|
1471
|
+
* ancestor that is no stacking context is made one (isolation), or a negative z-index would go behind its
|
|
1472
|
+
* background. Answers the edits (an override target and its style) and, when there is nothing to do, why:
|
|
1473
|
+
* 'alone' (it overlaps nothing), 'already' (it is in front / at the back already), 'blocked' (a branch cannot be
|
|
1474
|
+
* given a position without moving what is placed inside it).
|
|
1475
|
+
*/
|
|
1476
|
+
arrange({ handle, to }) {
|
|
1477
|
+
const el = nodeOf(handle);
|
|
1478
|
+
const c = el && clipOfNode(el);
|
|
1479
|
+
if (!el || !c) return null;
|
|
1480
|
+
const doc = el.ownerDocument;
|
|
1481
|
+
const front = to === 'front';
|
|
1482
|
+
const others = [];
|
|
1483
|
+
seeingAll(doc, () => {
|
|
1484
|
+
for (const o of layersOf(doc)) {
|
|
1485
|
+
if (o === el || o.contains(el) || el.contains(o) || invisible(o) || !kindOf(o)) continue;
|
|
1486
|
+
const above = drawnOver(o, el);
|
|
1487
|
+
if (above != null) others.push({ o, above });
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
if (!others.length) return { edits: [], reason: 'alone' };
|
|
1491
|
+
const todo = others.filter((x) => (front ? x.above : !x.above));
|
|
1492
|
+
if (!todo.length) return { edits: [], reason: 'already' };
|
|
1493
|
+
/** per branch to restack: the z-index it needs */
|
|
1494
|
+
const want = new Map();
|
|
1495
|
+
const isolate = new Set();
|
|
1496
|
+
for (const { o } of todo) {
|
|
1497
|
+
let lca = el.parentElement;
|
|
1498
|
+
while (lca && !lca.contains(o)) lca = lca.parentElement;
|
|
1499
|
+
if (!lca) continue;
|
|
1500
|
+
const branch = (x) => { let n = x; while (n.parentElement !== lca) n = n.parentElement; return n; };
|
|
1501
|
+
const a = branch(el);
|
|
1502
|
+
const b = branch(o);
|
|
1503
|
+
const z = front ? Math.max(zOf(b) ?? 0, 0) + 1 : Math.min(zOf(b) ?? 0, 0) - 1;
|
|
1504
|
+
const prev = want.get(a);
|
|
1505
|
+
want.set(a, prev == null ? z : front ? Math.max(prev, z) : Math.min(prev, z));
|
|
1506
|
+
if (!front && !stackingContext(lca)) isolate.add(lca);
|
|
1507
|
+
}
|
|
1508
|
+
const edits = [];
|
|
1509
|
+
for (const [a, z] of want) {
|
|
1510
|
+
const needs = !placedIn(a);
|
|
1511
|
+
if (needs && unsafe(a)) return { edits: [], reason: 'blocked' };
|
|
1512
|
+
const now = zOf(a);
|
|
1513
|
+
if (now != null && (front ? now >= z : now <= z)) continue;
|
|
1514
|
+
edits.push({ target: targetOf(a), style: { zIndex: z, ...(needs ? { position: 'relative' } : {}) } });
|
|
1515
|
+
}
|
|
1516
|
+
for (const lca of isolate) edits.push({ target: targetOf(lca), style: { isolation: 'isolate' } });
|
|
1517
|
+
if (!edits.length) return { edits: [], reason: 'already' };
|
|
1518
|
+
const kept = steady(c, edits, () => seeingAll(doc, () => todo.every(({ o }) => drawnOver(front ? el : o, front ? o : el) !== false)));
|
|
1519
|
+
return kept ? { edits: kept, reason: null } : { edits: [], reason: 'blocked' };
|
|
1520
|
+
},
|
|
1521
|
+
/**
|
|
1522
|
+
* The layers of the picture clips shown now, the top clip first; a page's as a tree, each level in the order it
|
|
1523
|
+
* is drawn, the one on top first (the Layers panel). See rowsOf.
|
|
1524
|
+
*/
|
|
1525
|
+
tree() {
|
|
1526
|
+
return shown().map((c) => ({ clip: c.id, kind: c.kind, layers: c.kind === 'page' && c.win?.document?.body ? rowsOf(c) : [] }));
|
|
1527
|
+
},
|
|
1528
|
+
/**
|
|
1529
|
+
* Restack siblings in the order a person put them in (the Layers panel): `order` the handles of the elements
|
|
1530
|
+
* under one parent, the top one first, `moved` the one that was moved. Only the moved one is given a z-index when
|
|
1531
|
+
* a z-index between its new neighbors puts it there; else all of them are numbered, bottom to top. Answers the
|
|
1532
|
+
* edits as arrange does, reason 'blocked' when they would move something, 'already' when the order is the page's.
|
|
1533
|
+
*/
|
|
1534
|
+
restack({ order, moved }) {
|
|
1535
|
+
const items = (order ?? []).map(nodeOf);
|
|
1536
|
+
const m = nodeOf(moved);
|
|
1537
|
+
const parentEl = m?.parentElement;
|
|
1538
|
+
const c = m && clipOfNode(m);
|
|
1539
|
+
if (!c || !parentEl || items.some((x) => !x || x.parentElement !== parentEl) || !items.includes(m)) return null;
|
|
1540
|
+
const want = [...items].reverse();
|
|
1541
|
+
const now = siblingOrder(items);
|
|
1542
|
+
if (now.every((x, i) => x === want[i])) return { edits: [], reason: 'already' };
|
|
1543
|
+
const at = want.indexOf(m);
|
|
1544
|
+
const below = want[at - 1] ?? null;
|
|
1545
|
+
const above = want[at + 1] ?? null;
|
|
1546
|
+
/* where m would be drawn with z-index z (it is given a position where it has none) */
|
|
1547
|
+
const keyWith = (z) => [z, z < 0 ? 0 : z === 0 ? 1 : 2];
|
|
1548
|
+
const before = (ka, a, kb, b) => (ka[0] - kb[0] || ka[1] - kb[1] || (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1)) < 0;
|
|
1549
|
+
/* m given a z-index is drawn all at once; its neighbors are drawn as far as what is in them reaches */
|
|
1550
|
+
const memo = new Map();
|
|
1551
|
+
const lowN = below && reachOf(below, memo);
|
|
1552
|
+
const highN = above && reachOf(above, memo);
|
|
1553
|
+
const fits = (z) => (!lowN || before(paintKey(lowN), lowN, keyWith(z), m)) && (!highN || before(keyWith(z), m, paintKey(highN), highN));
|
|
1554
|
+
const lo = lowN ? paintKey(lowN)[0] : (highN ? paintKey(highN)[0] - 1 : 0);
|
|
1555
|
+
const hi = highN ? paintKey(highN)[0] : lo + 1;
|
|
1556
|
+
/* the others keep their order among themselves: only m moves, by the first z-index that works (positive first:
|
|
1557
|
+
a z-index of 0 ties with the positioned layers, and a negative one needs its parent isolated) */
|
|
1558
|
+
const rest = want.filter((x) => x !== m);
|
|
1559
|
+
const others = now.filter((x) => x !== m);
|
|
1560
|
+
const sole = rest.every((x, i) => x === others[i])
|
|
1561
|
+
? [...new Set([lo + 1, lo, hi, hi - 1, lo - 1, hi + 1])].filter(fits).sort((a, b) => (a <= 0) - (b <= 0) || Math.abs(a) - Math.abs(b))
|
|
1562
|
+
: [];
|
|
1563
|
+
const editsOf = (plan) => {
|
|
1564
|
+
const edits = [];
|
|
1565
|
+
for (const [node, z] of plan) {
|
|
1566
|
+
const needs = !placedIn(node);
|
|
1567
|
+
if (needs && unsafe(node)) return null;
|
|
1568
|
+
if (!needs && zOf(node) === z) continue;
|
|
1569
|
+
edits.push({ target: targetOf(node), style: { zIndex: z, ...(needs ? { position: 'relative' } : {}) } });
|
|
1570
|
+
}
|
|
1571
|
+
if (plan.some(([, z]) => z < 0) && !stackingContext(parentEl)) edits.push({ target: targetOf(parentEl), style: { isolation: 'isolate' } });
|
|
1572
|
+
/* drawn as wanted: by the model, and where two of them overlap, by what the page's hit tests find */
|
|
1573
|
+
return steady(c, edits, () => siblingOrder(items).every((x, i) => x === want[i])
|
|
1574
|
+
&& seeingAll(parentEl.ownerDocument, () => want.every((lower, i) => want.slice(i + 1).every((upper) => drawnOver(upper, lower) !== false))));
|
|
1575
|
+
};
|
|
1576
|
+
let kept = null;
|
|
1577
|
+
for (const z of sole) if (!kept) kept = editsOf([[m, z]]);
|
|
1578
|
+
kept ??= editsOf(want.map((x, i) => [x, i + 1]));
|
|
1579
|
+
return kept ? { edits: kept, reason: null } : { edits: [], reason: 'blocked' };
|
|
1580
|
+
},
|
|
1581
|
+
textStart: startText,
|
|
1582
|
+
textFinish({ commit }) { finishText(Boolean(commit)); return true; },
|
|
1583
|
+
/**
|
|
1584
|
+
* What a page clip shows that it can change without opening the page (its properties are made of these): the CSS
|
|
1585
|
+
* custom properties its stylesheets set on `:root` (or `html`), each with its value now; its words, each layer of
|
|
1586
|
+
* text with its selector and its words now; and its pictures, each <img> and each box painted with one image, with
|
|
1587
|
+
* the address drawn now. Null when the clip is not a page on the picture now.
|
|
1588
|
+
*/
|
|
1589
|
+
controls({ clip }) {
|
|
1590
|
+
const c = clipById(clip);
|
|
1591
|
+
const doc = c?.kind === 'page' ? c.win?.document : null;
|
|
1592
|
+
if (!doc?.documentElement || !doc.body) return null;
|
|
1593
|
+
const names = new Set();
|
|
1594
|
+
const root = /^(?::root|html)$/i;
|
|
1595
|
+
const read = (rules) => {
|
|
1596
|
+
for (const rule of rules ?? []) {
|
|
1597
|
+
if (names.size >= CONTROLS) return;
|
|
1598
|
+
if (rule.cssRules && !rule.selectorText) { read(rule.cssRules); continue; }
|
|
1599
|
+
if (!rule.selectorText || !rule.selectorText.split(',').some((sel) => root.test(sel.trim()))) continue;
|
|
1600
|
+
for (const prop of rule.style) if (prop.startsWith('--')) names.add(prop);
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
for (const sheet of doc.styleSheets) {
|
|
1604
|
+
try { read(sheet.cssRules); } catch { /* a stylesheet from elsewhere: not readable */ }
|
|
1605
|
+
}
|
|
1606
|
+
const now = c.win.getComputedStyle(doc.documentElement);
|
|
1607
|
+
const vars = [...names].slice(0, CONTROLS).map((name) => ({ name, value: now.getPropertyValue(name).trim() }));
|
|
1608
|
+
const texts = [];
|
|
1609
|
+
for (const el of layersOf(doc)) {
|
|
1610
|
+
if (texts.length >= CONTROLS) break;
|
|
1611
|
+
if (kindOf(el) !== 'text' || isSvgText(el) || el.closest('svg')) continue;
|
|
1612
|
+
const value = textOf(el);
|
|
1613
|
+
if (!value) continue;
|
|
1614
|
+
const { at, n } = locOf(el);
|
|
1615
|
+
texts.push({ at, ...(n ? { n } : {}), label: el.id ? nameOf(el) : '', value });
|
|
1616
|
+
}
|
|
1617
|
+
const images = [];
|
|
1618
|
+
for (const el of layersOf(doc)) {
|
|
1619
|
+
if (images.length >= CONTROLS) break;
|
|
1620
|
+
if (el.closest('svg')) continue;
|
|
1621
|
+
const img = el.localName.toLowerCase() === 'img';
|
|
1622
|
+
const painted = img ? null : /^url\("?([^"]*)"?\)$/.exec(css(el).backgroundImage);
|
|
1623
|
+
const src = img ? el.currentSrc || el.src : painted?.[1] ?? '';
|
|
1624
|
+
if (!src) continue;
|
|
1625
|
+
const { at, n } = locOf(el);
|
|
1626
|
+
images.push({ at, ...(n ? { n } : {}), label: el.id ? nameOf(el) : (img && el.alt) || '', kind: img ? 'img' : 'bg', src });
|
|
1627
|
+
}
|
|
1628
|
+
return { vars, texts, images, page: c.win.location.href };
|
|
1629
|
+
},
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
window.addEventListener('message', async (e) => {
|
|
1633
|
+
if (e.origin !== EDITOR || e.source !== parent || e.data?.source !== 'openfilm-studio' || e.data.type !== 'stage') return;
|
|
1634
|
+
const { seq, op, ...args } = e.data;
|
|
1635
|
+
const fn = Object.hasOwn(ops, op) ? ops[op] : null;
|
|
1636
|
+
let result = null;
|
|
1637
|
+
try {
|
|
1638
|
+
if (window.film) await window.film.ready;
|
|
1639
|
+
listenAll();
|
|
1640
|
+
/* the line being typed was drawn anew (an undo, the page rewritten): what was typed is kept, the box closes */
|
|
1641
|
+
if (editing && !editing.el.isConnected) finishText(true);
|
|
1642
|
+
result = fn ? fn(args) ?? null : null;
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
post({ type: 'stage', seq, result: null, error: String(error?.message ?? error) });
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
post({ type: 'stage', seq, result });
|
|
1648
|
+
});
|
|
1649
|
+
})();
|