From 0aae91f4f21f268c4b9958fd19670ed89fd7b60c Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Fri, 10 Jul 2026 17:01:28 +0300 Subject: [PATCH] feat(editor): add blend mode and mask controls - Add selection mask commands, shortcuts, context-menu access, and mask type controls - Add mixed-selection blend mode editing with single-step undo - Document the headless mask composable and localize the new controls --- CHANGELOG.md | 1 + .../programmable/sdk/api/composables/index.md | 1 + .../sdk/api/composables/use-mask.md | 28 ++++ packages/vue/README.md | 1 + .../vue/src/controls/appearance/helpers.ts | 38 ++++- packages/vue/src/controls/mask/use.ts | 23 +++ packages/vue/src/editor/commands/registry.ts | 5 + packages/vue/src/editor/commands/selection.ts | 37 +++++ packages/vue/src/editor/commands/types.ts | 1 + packages/vue/src/editor/menu-model/canvas.ts | 1 + .../src/editor/selection-capabilities/use.ts | 1 + .../vue/src/i18n/locales/de/commands.json | 4 +- packages/vue/src/i18n/locales/de/panels.json | 25 +++- .../vue/src/i18n/locales/es/commands.json | 4 +- packages/vue/src/i18n/locales/es/panels.json | 25 +++- .../vue/src/i18n/locales/fr/commands.json | 4 +- packages/vue/src/i18n/locales/fr/panels.json | 25 +++- .../vue/src/i18n/locales/it/commands.json | 4 +- packages/vue/src/i18n/locales/it/panels.json | 25 +++- .../vue/src/i18n/locales/ja/commands.json | 4 +- packages/vue/src/i18n/locales/ja/panels.json | 25 +++- .../vue/src/i18n/locales/pl/commands.json | 4 +- packages/vue/src/i18n/locales/pl/panels.json | 25 +++- .../vue/src/i18n/locales/ru/commands.json | 4 +- packages/vue/src/i18n/locales/ru/panels.json | 25 +++- .../vue/src/i18n/locales/zh-cn/commands.json | 4 +- .../vue/src/i18n/locales/zh-cn/panels.json | 25 +++- packages/vue/src/i18n/messages/commands.ts | 2 + packages/vue/src/i18n/messages/panels.ts | 25 ++++ packages/vue/src/index.ts | 1 + src/app/shell/keyboard/registry.ts | 1 + src/components/DesignPanel.vue | 9 +- .../properties/AppearanceSection.vue | 133 ++++++++++++------ src/components/properties/MaskSection.vue | 39 +++++ .../properties/SelectionActionsControl.vue | 27 ++++ src/components/ui/AppSelect.vue | 4 +- tests/e2e/design/panel.spec.ts | 88 ++++++++++++ .../vue/editor/menu-model/canvas.test.ts | 1 + 38 files changed, 633 insertions(+), 66 deletions(-) create mode 100644 packages/docs/programmable/sdk/api/composables/use-mask.md create mode 100644 packages/vue/src/controls/mask/use.ts create mode 100644 src/components/properties/MaskSection.vue create mode 100644 src/components/properties/SelectionActionsControl.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index f63e45d49..4f4834e10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Add richer Design JSX authoring for components, variables, structured fills, gradients, shadows, and blur effects. - Add overlap analysis for finding layout collisions and overflowing children from the CLI, AI tools, and MCP. - Add saved per-node export settings for repeat exports. +- Add Design panel controls for layer blend modes and alpha, vector, and luminance masks. - Add desktop image drag-and-drop into the Tauri app window. - Add open-document discovery for live CLI and MCP automation so agents can target the intended document and page. - Publish lower-level SceneGraph, Pen, Kiwi, Fig, and DOM/CSS functionality through clearer package boundaries for SDK and automation consumers. diff --git a/packages/docs/programmable/sdk/api/composables/index.md b/packages/docs/programmable/sdk/api/composables/index.md index ca233b64a..2edd7f12a 100644 --- a/packages/docs/programmable/sdk/api/composables/index.md +++ b/packages/docs/programmable/sdk/api/composables/index.md @@ -27,6 +27,7 @@ These are the main composables most `@open-pencil/vue` consumers will use. - [usePosition](./use-position) - [useLayout](./use-layout) - [useAppearance](./use-appearance) +- [useMask](./use-mask) - [useTypography](./use-typography) - [useExport](./use-export) - [useFillControls](./use-fill-controls) diff --git a/packages/docs/programmable/sdk/api/composables/use-mask.md b/packages/docs/programmable/sdk/api/composables/use-mask.md new file mode 100644 index 000000000..eb6cd332b --- /dev/null +++ b/packages/docs/programmable/sdk/api/composables/use-mask.md @@ -0,0 +1,28 @@ +--- +title: useMask +description: Read and update mask state for the selected node. +--- + +# useMask + +`useMask()` provides headless state and actions for mask controls in a property panel. + +## Usage + +```ts +import { useMask } from '@open-pencil/vue' + +const { active, maskType, setMaskType } = useMask() +``` + +- `active` is `true` when exactly one selected node is being used as a mask. +- `maskType` is the selected node's `ALPHA`, `VECTOR`, or `LUMINANCE` mask type. +- `setMaskType(type)` updates the active mask with undo support. + +Use the `selection.toggleMask` editor command to turn masking on or off so toolbar, context-menu, and keyboard behavior stays consistent. + +## Related APIs + +- [useAppearance](./use-appearance) +- [useEditorCommands](./use-editor-commands) +- [SDK API Overview](../) diff --git a/packages/vue/README.md b/packages/vue/README.md index dcf27000f..ff9aa509c 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -115,6 +115,7 @@ These are the main APIs most SDK consumers should start with. - `usePosition()` - `useLayout()` - `useAppearance()` +- `useMask()` - `useTypography()` - `useExport()` - `useFillControls()` diff --git a/packages/vue/src/controls/appearance/helpers.ts b/packages/vue/src/controls/appearance/helpers.ts index 79deaad2d..6959c0de3 100644 --- a/packages/vue/src/controls/appearance/helpers.ts +++ b/packages/vue/src/controls/appearance/helpers.ts @@ -2,7 +2,7 @@ import { computed } from 'vue' import type { ComputedRef } from 'vue' import type { Editor } from '@open-pencil/core/editor' -import type { SceneNode } from '@open-pencil/scene-graph' +import type { BlendMode, SceneNode } from '@open-pencil/scene-graph' import { MIXED, type MixedValue } from '#vue/controls/node-props/use' @@ -46,16 +46,42 @@ export function createAppearanceState({ node, nodes, isMulti, merged }: Appearan return v === MIXED ? MIXED : Math.round(v * 100) }) + const blendModeValue = computed(() => { + const v = merged('blendMode') + return v === MIXED ? MIXED : v + }) + const visibilityState = computed<'visible' | 'hidden' | 'mixed'>(() => { const v = merged('visible') if (v === MIXED) return 'mixed' return v ? 'visible' : 'hidden' }) - return { hasCornerRadius, independentCorners, cornerRadiusValue, opacityPercent, visibilityState } + return { + hasCornerRadius, + independentCorners, + cornerRadiusValue, + opacityPercent, + blendModeValue, + visibilityState + } } export function createAppearanceActions({ editor, node, nodes, isMulti }: AppearanceActionOptions) { + function setBlendMode(value: BlendMode) { + const selected = node.value + const targets = isMulti.value ? nodes.value : [] + if (!isMulti.value && selected) targets.push(selected) + const changed = targets.filter((target) => target.blendMode !== value) + if (changed.length === 0) return + + editor.undo.runBatch('Change blend mode', () => { + for (const target of changed) { + editor.updateNodeWithUndo(target.id, { blendMode: value }, 'Change blend mode') + } + }) + } + function toggleVisibility() { if (isMulti.value) { const liveNodes = nodes.value @@ -135,5 +161,11 @@ export function createAppearanceActions({ editor, node, nodes, isMulti }: Appear } } - return { toggleVisibility, toggleIndependentCorners, updateCornerProp, commitCornerProp } + return { + setBlendMode, + toggleVisibility, + toggleIndependentCorners, + updateCornerProp, + commitCornerProp + } } diff --git a/packages/vue/src/controls/mask/use.ts b/packages/vue/src/controls/mask/use.ts new file mode 100644 index 000000000..697805b94 --- /dev/null +++ b/packages/vue/src/controls/mask/use.ts @@ -0,0 +1,23 @@ +import { computed } from 'vue' + +import type { MaskType } from '@open-pencil/scene-graph' + +import { useEditor } from '#vue/editor/context' +import { useSelectionState } from '#vue/editor/selection-state/use' + +/** Headless state and actions for the selected mask node. */ +export function useMask() { + const editor = useEditor() + const { selectedNode } = useSelectionState() + + const active = computed(() => selectedNode.value?.isMask === true) + const maskType = computed(() => selectedNode.value?.maskType ?? 'ALPHA') + + function setMaskType(value: MaskType) { + const node = selectedNode.value + if (!node?.isMask || node.maskType === value) return + editor.updateNodeWithUndo(node.id, { maskType: value }, 'Change mask type') + } + + return { active, maskType, setMaskType } +} diff --git a/packages/vue/src/editor/commands/registry.ts b/packages/vue/src/editor/commands/registry.ts index e288c58d2..77e4de0ee 100644 --- a/packages/vue/src/editor/commands/registry.ts +++ b/packages/vue/src/editor/commands/registry.ts @@ -33,6 +33,11 @@ export const EDITOR_COMMAND_METADATA = { 'selection.goToMainComponent': {}, 'selection.createInstance': {}, 'selection.wrapInAutoLayout': { shortcut: 'SHIFT+A', keybinding: 'Shift+KeyA' }, + 'selection.toggleMask': { + shortcut: 'MOD+ALT+M', + keybinding: ['Control+Meta+KeyM', '$mod+Alt+KeyM'], + contextTestId: 'context-toggle-mask' + }, 'selection.bringToFront': { shortcut: ']', keybinding: 'BracketRight', diff --git a/packages/vue/src/editor/commands/selection.ts b/packages/vue/src/editor/commands/selection.ts index 17537ab50..151984b02 100644 --- a/packages/vue/src/editor/commands/selection.ts +++ b/packages/vue/src/editor/commands/selection.ts @@ -1,8 +1,28 @@ +import type { SceneNode } from '@open-pencil/scene-graph' + import type { EditorCommandMapOptions } from './context' import type { EditorCommand, EditorCommandId } from './types' type SelectionCommandId = Extract +function selectedMaskTarget(nodes: SceneNode[], editor: EditorCommandMapOptions['editor']) { + if (nodes.length === 0) return null + if (nodes.length === 1) return nodes[0] + + const [first] = nodes + const byId = new Map(nodes.map((node) => [node.id, node])) + const selectedParents = new Set(nodes.map((node) => node.parentId)) + if (selectedParents.size !== 1) return first + const parentId = first.parentId + if (!parentId) return first + + for (const sibling of editor.graph.getChildren(parentId)) { + const selected = byId.get(sibling.id) + if (selected) return selected + } + return first +} + export function createSelectionCommands({ editor, selection, @@ -111,6 +131,23 @@ export function createSelectionCommands({ enabled: capabilities.canWrapInAutoLayout, run: () => editor.wrapInAutoLayout() }, + 'selection.toggleMask': { + id: 'selection.toggleMask', + get label() { + const target = selectedMaskTarget(editor.getSelectedNodes(), editor) + return target?.isMask ? t.value.removeMask : t.value.useAsMask + }, + enabled: capabilities.canToggleMask, + run: () => { + const target = selectedMaskTarget(editor.getSelectedNodes(), editor) + if (!target) return + editor.updateNodeWithUndo( + target.id, + { isMask: !target.isMask }, + target.isMask ? 'Remove mask' : 'Use as mask' + ) + } + }, 'selection.bringToFront': { id: 'selection.bringToFront', get label() { diff --git a/packages/vue/src/editor/commands/types.ts b/packages/vue/src/editor/commands/types.ts index aa0cd74f3..2e341182c 100644 --- a/packages/vue/src/editor/commands/types.ts +++ b/packages/vue/src/editor/commands/types.ts @@ -15,6 +15,7 @@ export type EditorCommandId = | 'selection.detachInstance' | 'selection.goToMainComponent' | 'selection.wrapInAutoLayout' + | 'selection.toggleMask' | 'selection.bringToFront' | 'selection.sendToBack' | 'selection.toggleVisibility' diff --git a/packages/vue/src/editor/menu-model/canvas.ts b/packages/vue/src/editor/menu-model/canvas.ts index 96379fc63..3d69a6cd3 100644 --- a/packages/vue/src/editor/menu-model/canvas.ts +++ b/packages/vue/src/editor/menu-model/canvas.ts @@ -36,6 +36,7 @@ const CANVAS_MENU_GROUPS = [ 'selection.frameSelection', 'selection.ungroupWhenGroup', 'selection.wrapInAutoLayout', + 'selection.toggleMask', 'selection.flatten', 'selection.outlineText', 'selection.outlineStroke' diff --git a/packages/vue/src/editor/selection-capabilities/use.ts b/packages/vue/src/editor/selection-capabilities/use.ts index a033a29f8..a11653f12 100644 --- a/packages/vue/src/editor/selection-capabilities/use.ts +++ b/packages/vue/src/editor/selection-capabilities/use.ts @@ -37,6 +37,7 @@ export function useSelectionCapabilities() { canCreateComponentSet: selection.canCreateComponentSet, canDetachInstance: computed(() => selection.isInstance.value), canWrapInAutoLayout: computed(() => hasSelection.value), + canToggleMask: computed(() => hasSelection.value), canBringToFront: computed(() => hasSelection.value), canSendToBack: computed(() => hasSelection.value), canToggleVisibility: computed(() => hasSelection.value), diff --git a/packages/vue/src/i18n/locales/de/commands.json b/packages/vue/src/i18n/locales/de/commands.json index 30df37823..642538812 100644 --- a/packages/vue/src/i18n/locales/de/commands.json +++ b/packages/vue/src/i18n/locales/de/commands.json @@ -31,5 +31,7 @@ "flipVertical": "Vertikal spiegeln", "flattenSelection": "Abflachen", "outlineText": "Text in Pfade umwandeln", - "outlineStroke": "Kontur in Pfad umwandeln" + "outlineStroke": "Kontur in Pfad umwandeln", + "useAsMask": "Als Maske verwenden", + "removeMask": "Maske entfernen" } diff --git a/packages/vue/src/i18n/locales/de/panels.json b/packages/vue/src/i18n/locales/de/panels.json index a5346376e..2f6528972 100644 --- a/packages/vue/src/i18n/locales/de/panels.json +++ b/packages/vue/src/i18n/locales/de/panels.json @@ -152,5 +152,28 @@ "exportScale": "Exportmaßstab", "exportFormat": "Exportformat", "gap": "Abstand", - "searchFonts": "Schriftarten suchen..." + "searchFonts": "Schriftarten suchen...", + "blendMode": "Blendmodus", + "mask": "Maske", + "maskType": "Maskentyp", + "maskTypeAlpha": "Alpha", + "maskTypeVector": "Vektor", + "maskTypeLuminance": "Luminanz", + "blendModePassThrough": "Durchreichen", + "blendModeNormal": "Normal", + "blendModeDarken": "Abdunkeln", + "blendModeMultiply": "Multiplizieren", + "blendModeColorBurn": "Farbig nachbelichten", + "blendModeLighten": "Aufhellen", + "blendModeScreen": "Negativ multiplizieren", + "blendModeColorDodge": "Farbig abwedeln", + "blendModeOverlay": "Überlagern", + "blendModeSoftLight": "Weiches Licht", + "blendModeHardLight": "Hartes Licht", + "blendModeDifference": "Differenz", + "blendModeExclusion": "Ausschluss", + "blendModeHue": "Farbton", + "blendModeSaturation": "Sättigung", + "blendModeColor": "Farbe", + "blendModeLuminosity": "Luminanz" } diff --git a/packages/vue/src/i18n/locales/es/commands.json b/packages/vue/src/i18n/locales/es/commands.json index 28d400bdf..427c5095f 100644 --- a/packages/vue/src/i18n/locales/es/commands.json +++ b/packages/vue/src/i18n/locales/es/commands.json @@ -31,5 +31,7 @@ "flipVertical": "Voltear verticalmente", "flattenSelection": "Aplanar", "outlineText": "Contornear texto", - "outlineStroke": "Cortornear trazo" + "outlineStroke": "Cortornear trazo", + "useAsMask": "Usar como máscara", + "removeMask": "Quitar máscara" } diff --git a/packages/vue/src/i18n/locales/es/panels.json b/packages/vue/src/i18n/locales/es/panels.json index 02a0b65d4..870bbfa4a 100644 --- a/packages/vue/src/i18n/locales/es/panels.json +++ b/packages/vue/src/i18n/locales/es/panels.json @@ -152,5 +152,28 @@ "expandEffectSettings": "Expandir ajustes del efecto", "collapseEffectSettings": "Contraer ajustes del efecto", "toggleExportPreview": "Alternar vista previa de exportación", - "searchFonts": "Buscar fuentes..." + "searchFonts": "Buscar fuentes...", + "blendMode": "Modo de fusión", + "mask": "Máscara", + "maskType": "Tipo de máscara", + "maskTypeAlpha": "Alfa", + "maskTypeVector": "Vectorial", + "maskTypeLuminance": "Luminancia", + "blendModePassThrough": "Pasar a través", + "blendModeNormal": "Normal", + "blendModeDarken": "Oscurecer", + "blendModeMultiply": "Multiplicar", + "blendModeColorBurn": "Subexponer color", + "blendModeLighten": "Aclarar", + "blendModeScreen": "Trama", + "blendModeColorDodge": "Sobreexponer color", + "blendModeOverlay": "Superponer", + "blendModeSoftLight": "Luz suave", + "blendModeHardLight": "Luz fuerte", + "blendModeDifference": "Diferencia", + "blendModeExclusion": "Exclusión", + "blendModeHue": "Tono", + "blendModeSaturation": "Saturación", + "blendModeColor": "Color", + "blendModeLuminosity": "Luminosidad" } diff --git a/packages/vue/src/i18n/locales/fr/commands.json b/packages/vue/src/i18n/locales/fr/commands.json index 018799449..fd470a22b 100644 --- a/packages/vue/src/i18n/locales/fr/commands.json +++ b/packages/vue/src/i18n/locales/fr/commands.json @@ -31,5 +31,7 @@ "flipVertical": "Retourner verticalement", "flattenSelection": "Aplatir", "outlineText": "Vectoriser le texte", - "outlineStroke": "Vectoriser le contour" + "outlineStroke": "Vectoriser le contour", + "useAsMask": "Utiliser comme masque", + "removeMask": "Retirer le masque" } diff --git a/packages/vue/src/i18n/locales/fr/panels.json b/packages/vue/src/i18n/locales/fr/panels.json index 163b2bd4a..56d842c3b 100644 --- a/packages/vue/src/i18n/locales/fr/panels.json +++ b/packages/vue/src/i18n/locales/fr/panels.json @@ -152,5 +152,28 @@ "exportScale": "Échelle d’export", "exportFormat": "Format d’export", "gap": "Espacement", - "searchFonts": "Rechercher des polices..." + "searchFonts": "Rechercher des polices...", + "blendMode": "Mode de fusion", + "mask": "Masque", + "maskType": "Type de masque", + "maskTypeAlpha": "Alpha", + "maskTypeVector": "Vectoriel", + "maskTypeLuminance": "Luminance", + "blendModePassThrough": "Traverser", + "blendModeNormal": "Normal", + "blendModeDarken": "Obscurcir", + "blendModeMultiply": "Produit", + "blendModeColorBurn": "Densité couleur +", + "blendModeLighten": "Éclaircir", + "blendModeScreen": "Écran", + "blendModeColorDodge": "Densité couleur -", + "blendModeOverlay": "Incrustation", + "blendModeSoftLight": "Lumière tamisée", + "blendModeHardLight": "Lumière crue", + "blendModeDifference": "Différence", + "blendModeExclusion": "Exclusion", + "blendModeHue": "Teinte", + "blendModeSaturation": "Saturation", + "blendModeColor": "Couleur", + "blendModeLuminosity": "Luminosité" } diff --git a/packages/vue/src/i18n/locales/it/commands.json b/packages/vue/src/i18n/locales/it/commands.json index 0f5b9c905..a9542c114 100644 --- a/packages/vue/src/i18n/locales/it/commands.json +++ b/packages/vue/src/i18n/locales/it/commands.json @@ -31,5 +31,7 @@ "flipVertical": "Rifletti verticalmente", "flattenSelection": "Appiattisci", "outlineText": "Converti testo in tracciato", - "outlineStroke": "Converti traccia in contorno" + "outlineStroke": "Converti traccia in contorno", + "useAsMask": "Usa come maschera", + "removeMask": "Rimuovi maschera" } diff --git a/packages/vue/src/i18n/locales/it/panels.json b/packages/vue/src/i18n/locales/it/panels.json index 3ed1b5d8d..0a6ef29c7 100644 --- a/packages/vue/src/i18n/locales/it/panels.json +++ b/packages/vue/src/i18n/locales/it/panels.json @@ -152,5 +152,28 @@ "exportScale": "Scala esportazione", "exportFormat": "Formato esportazione", "gap": "Spazio", - "searchFonts": "Cerca font..." + "searchFonts": "Cerca font...", + "blendMode": "Metodo di fusione", + "mask": "Maschera", + "maskType": "Tipo di maschera", + "maskTypeAlpha": "Alfa", + "maskTypeVector": "Vettore", + "maskTypeLuminance": "Luminanza", + "blendModePassThrough": "Attraversa", + "blendModeNormal": "Normale", + "blendModeDarken": "Scurisci", + "blendModeMultiply": "Moltiplica", + "blendModeColorBurn": "Brucia colore", + "blendModeLighten": "Schiarisci", + "blendModeScreen": "Scolora", + "blendModeColorDodge": "Scherma colore", + "blendModeOverlay": "Sovrapponi", + "blendModeSoftLight": "Luce soffusa", + "blendModeHardLight": "Luce intensa", + "blendModeDifference": "Differenza", + "blendModeExclusion": "Esclusione", + "blendModeHue": "Tonalità", + "blendModeSaturation": "Saturazione", + "blendModeColor": "Colore", + "blendModeLuminosity": "Luminosità" } diff --git a/packages/vue/src/i18n/locales/ja/commands.json b/packages/vue/src/i18n/locales/ja/commands.json index ed79a2cb4..426d1a306 100644 --- a/packages/vue/src/i18n/locales/ja/commands.json +++ b/packages/vue/src/i18n/locales/ja/commands.json @@ -31,5 +31,7 @@ "flipVertical": "垂直方向に反転", "flattenSelection": "フラット化", "outlineText": "テキストのアウトライン化", - "outlineStroke": "線の境界線をプレビュー (パスのアウトライン化)" + "outlineStroke": "線の境界線をプレビュー (パスのアウトライン化)", + "useAsMask": "マスクとして使用", + "removeMask": "マスクを解除" } diff --git a/packages/vue/src/i18n/locales/ja/panels.json b/packages/vue/src/i18n/locales/ja/panels.json index 8de509931..17b7269f7 100644 --- a/packages/vue/src/i18n/locales/ja/panels.json +++ b/packages/vue/src/i18n/locales/ja/panels.json @@ -152,5 +152,28 @@ "expandEffectSettings": "エフェクト設定を展開", "collapseEffectSettings": "エフェクト設定を折りたたむ", "toggleExportPreview": "エクスポートプレビューの切り替え", - "searchFonts": "フォントを検索..." + "searchFonts": "フォントを検索...", + "blendMode": "ブレンドモード", + "mask": "マスク", + "maskType": "マスクの種類", + "maskTypeAlpha": "アルファ", + "maskTypeVector": "ベクター", + "maskTypeLuminance": "輝度", + "blendModePassThrough": "通過", + "blendModeNormal": "通常", + "blendModeDarken": "比較(暗)", + "blendModeMultiply": "乗算", + "blendModeColorBurn": "焼き込みカラー", + "blendModeLighten": "比較(明)", + "blendModeScreen": "スクリーン", + "blendModeColorDodge": "覆い焼きカラー", + "blendModeOverlay": "オーバーレイ", + "blendModeSoftLight": "ソフトライト", + "blendModeHardLight": "ハードライト", + "blendModeDifference": "差の絶対値", + "blendModeExclusion": "除外", + "blendModeHue": "色相", + "blendModeSaturation": "彩度", + "blendModeColor": "カラー", + "blendModeLuminosity": "輝度" } diff --git a/packages/vue/src/i18n/locales/pl/commands.json b/packages/vue/src/i18n/locales/pl/commands.json index 6fbafaa2d..32482b1b6 100644 --- a/packages/vue/src/i18n/locales/pl/commands.json +++ b/packages/vue/src/i18n/locales/pl/commands.json @@ -31,5 +31,7 @@ "flipVertical": "Odbij pionowo", "flattenSelection": "Spłaszcz", "outlineText": "Kontur tekstu", - "outlineStroke": "Kontur obrysu" + "outlineStroke": "Kontur obrysu", + "useAsMask": "Użyj jako maski", + "removeMask": "Usuń maskę" } diff --git a/packages/vue/src/i18n/locales/pl/panels.json b/packages/vue/src/i18n/locales/pl/panels.json index 6727f0dde..01efe4c78 100644 --- a/packages/vue/src/i18n/locales/pl/panels.json +++ b/packages/vue/src/i18n/locales/pl/panels.json @@ -152,5 +152,28 @@ "exportScale": "Skala eksportu", "exportFormat": "Format eksportu", "gap": "Odstęp", - "searchFonts": "Szukaj fontów..." + "searchFonts": "Szukaj fontów...", + "blendMode": "Tryb mieszania", + "mask": "Maska", + "maskType": "Typ maski", + "maskTypeAlpha": "Alfa", + "maskTypeVector": "Wektorowa", + "maskTypeLuminance": "Luminancja", + "blendModePassThrough": "Przepuszczanie", + "blendModeNormal": "Normalny", + "blendModeDarken": "Ciemniej", + "blendModeMultiply": "Mnożenie", + "blendModeColorBurn": "Ściemnianie koloru", + "blendModeLighten": "Jaśniej", + "blendModeScreen": "Ekran", + "blendModeColorDodge": "Rozjaśnianie koloru", + "blendModeOverlay": "Nakładka", + "blendModeSoftLight": "Łagodne światło", + "blendModeHardLight": "Ostre światło", + "blendModeDifference": "Różnica", + "blendModeExclusion": "Wykluczenie", + "blendModeHue": "Barwa", + "blendModeSaturation": "Nasycenie", + "blendModeColor": "Kolor", + "blendModeLuminosity": "Jasność" } diff --git a/packages/vue/src/i18n/locales/ru/commands.json b/packages/vue/src/i18n/locales/ru/commands.json index 455cbab9d..57e59500f 100644 --- a/packages/vue/src/i18n/locales/ru/commands.json +++ b/packages/vue/src/i18n/locales/ru/commands.json @@ -31,5 +31,7 @@ "flipVertical": "Отразить по вертикали", "flattenSelection": "Сгладить", "outlineText": "Преобразовать текст в контуры", - "outlineStroke": "Преобразовать обводку в контур" + "outlineStroke": "Преобразовать обводку в контур", + "useAsMask": "Использовать как маску", + "removeMask": "Убрать маску" } diff --git a/packages/vue/src/i18n/locales/ru/panels.json b/packages/vue/src/i18n/locales/ru/panels.json index 0b1adb702..be7aafd8d 100644 --- a/packages/vue/src/i18n/locales/ru/panels.json +++ b/packages/vue/src/i18n/locales/ru/panels.json @@ -152,5 +152,28 @@ "exportScale": "Масштаб экспорта", "exportFormat": "Формат экспорта", "gap": "Интервал", - "searchFonts": "Поиск шрифтов..." + "searchFonts": "Поиск шрифтов...", + "blendMode": "Режим наложения", + "mask": "Маска", + "maskType": "Тип маски", + "maskTypeAlpha": "Альфа-канал", + "maskTypeVector": "Вектор", + "maskTypeLuminance": "Яркость", + "blendModePassThrough": "Сквозной", + "blendModeNormal": "Обычный", + "blendModeDarken": "Затемнение", + "blendModeMultiply": "Умножение", + "blendModeColorBurn": "Затемнение основы", + "blendModeLighten": "Осветление", + "blendModeScreen": "Экран", + "blendModeColorDodge": "Осветление основы", + "blendModeOverlay": "Перекрытие", + "blendModeSoftLight": "Мягкий свет", + "blendModeHardLight": "Жёсткий свет", + "blendModeDifference": "Разница", + "blendModeExclusion": "Исключение", + "blendModeHue": "Тон", + "blendModeSaturation": "Насыщенность", + "blendModeColor": "Цвет", + "blendModeLuminosity": "Яркость" } diff --git a/packages/vue/src/i18n/locales/zh-cn/commands.json b/packages/vue/src/i18n/locales/zh-cn/commands.json index 9ac5a0a42..99138a415 100644 --- a/packages/vue/src/i18n/locales/zh-cn/commands.json +++ b/packages/vue/src/i18n/locales/zh-cn/commands.json @@ -31,5 +31,7 @@ "flipVertical": "垂直翻转", "flattenSelection": "扁平化", "outlineText": "文本转轮廓", - "outlineStroke": "描边转轮廓" + "outlineStroke": "描边转轮廓", + "useAsMask": "用作蒙版", + "removeMask": "取消蒙版" } diff --git a/packages/vue/src/i18n/locales/zh-cn/panels.json b/packages/vue/src/i18n/locales/zh-cn/panels.json index ec87c7154..7d27cdf71 100644 --- a/packages/vue/src/i18n/locales/zh-cn/panels.json +++ b/packages/vue/src/i18n/locales/zh-cn/panels.json @@ -152,5 +152,28 @@ "exportScale": "导出缩放", "exportFormat": "导出格式", "gap": "间距", - "searchFonts": "搜索字体..." + "searchFonts": "搜索字体...", + "blendMode": "混合模式", + "mask": "蒙版", + "maskType": "蒙版类型", + "maskTypeAlpha": "Alpha", + "maskTypeVector": "矢量", + "maskTypeLuminance": "亮度", + "blendModePassThrough": "穿透", + "blendModeNormal": "正常", + "blendModeDarken": "变暗", + "blendModeMultiply": "正片叠底", + "blendModeColorBurn": "颜色加深", + "blendModeLighten": "变亮", + "blendModeScreen": "滤色", + "blendModeColorDodge": "颜色减淡", + "blendModeOverlay": "叠加", + "blendModeSoftLight": "柔光", + "blendModeHardLight": "强光", + "blendModeDifference": "差值", + "blendModeExclusion": "排除", + "blendModeHue": "色相", + "blendModeSaturation": "饱和度", + "blendModeColor": "颜色", + "blendModeLuminosity": "明度" } diff --git a/packages/vue/src/i18n/messages/commands.ts b/packages/vue/src/i18n/messages/commands.ts index 00721be53..da696cfa6 100644 --- a/packages/vue/src/i18n/messages/commands.ts +++ b/packages/vue/src/i18n/messages/commands.ts @@ -16,6 +16,8 @@ export const commandMessageDefaults = { detachInstance: 'Detach instance', goToMainComponent: 'Go to main component', addAutoLayout: 'Add auto layout', + useAsMask: 'Use as mask', + removeMask: 'Remove mask', bringToFront: 'Bring to front', sendToBack: 'Send to back', showHide: 'Show/Hide', diff --git a/packages/vue/src/i18n/messages/panels.ts b/packages/vue/src/i18n/messages/panels.ts index a950edfe3..440b42c3c 100644 --- a/packages/vue/src/i18n/messages/panels.ts +++ b/packages/vue/src/i18n/messages/panels.ts @@ -31,6 +31,7 @@ export const panelMessageDefaults = { width: 'Width', height: 'Height', opacity: 'Opacity', + blendMode: 'Blend mode', radius: 'Radius', spread: 'Spread', @@ -43,6 +44,7 @@ export const panelMessageDefaults = { fill: 'Fill', stroke: 'Stroke', effects: 'Effects', + mask: 'Mask', export: 'Export', typography: 'Typography', variables: 'Variables', @@ -68,6 +70,29 @@ export const panelMessageDefaults = { backgroundBlur: 'Background blur', foregroundBlur: 'Foreground blur', + maskType: 'Mask type', + maskTypeAlpha: 'Alpha', + maskTypeVector: 'Vector', + maskTypeLuminance: 'Luminance', + + blendModePassThrough: 'Pass through', + blendModeNormal: 'Normal', + blendModeDarken: 'Darken', + blendModeMultiply: 'Multiply', + blendModeColorBurn: 'Color burn', + blendModeLighten: 'Lighten', + blendModeScreen: 'Screen', + blendModeColorDodge: 'Color dodge', + blendModeOverlay: 'Overlay', + blendModeSoftLight: 'Soft light', + blendModeHardLight: 'Hard light', + blendModeDifference: 'Difference', + blendModeExclusion: 'Exclusion', + blendModeHue: 'Hue', + blendModeSaturation: 'Saturation', + blendModeColor: 'Color', + blendModeLuminosity: 'Luminosity', + strokeType: 'Stroke type', strokeWeight: 'Stroke weight', diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index b797d71fd..2a5a7c1e7 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -75,6 +75,7 @@ export { usePosition } from '#vue/controls/position/use' export { useLayout } from '#vue/controls/layout/use' export type { SizeLimitProp } from '#vue/controls/layout/helpers' export { useAppearance } from '#vue/controls/appearance/use' +export { useMask } from '#vue/controls/mask/use' export { useTypography } from '#vue/controls/typography/use' export type { UseTypographyOptions } from '#vue/controls/typography/use' export { useExport } from '#vue/document/export/use' diff --git a/src/app/shell/keyboard/registry.ts b/src/app/shell/keyboard/registry.ts index 7dbb0e612..c9b44fe5e 100644 --- a/src/app/shell/keyboard/registry.ts +++ b/src/app/shell/keyboard/registry.ts @@ -77,6 +77,7 @@ export function registerKeyboardShortcuts(options: KeyboardShortcutOptions) { 'selection.createComponent', 'selection.detachInstance', 'selection.createComponentSet', + 'selection.toggleMask', 'selection.toggleVisibility', 'selection.toggleLock', 'selection.flipHorizontal', diff --git a/src/components/DesignPanel.vue b/src/components/DesignPanel.vue index 0c5ea7f4f..146345f6f 100644 --- a/src/components/DesignPanel.vue +++ b/src/components/DesignPanel.vue @@ -4,14 +4,15 @@ import { computed, ref } from 'vue' import { useI18n, useSelectionState, useEditorCommands } from '@open-pencil/vue' import VariablesDialog from './variables/VariablesDialog.vue' -import BooleanOperationsControl from './properties/BooleanOperationsControl.vue' import AppearanceSection from './properties/AppearanceSection.vue' import EffectsSection from './properties/EffectsSection.vue' import ExportSection from './properties/ExportSection.vue' import FillSection from './properties/FillSection.vue' import LayoutSection from './properties/LayoutSection/LayoutSection.vue' +import MaskSection from './properties/MaskSection.vue' import PageSection from './properties/PageSection.vue' import PositionSection from './properties/PositionSection.vue' +import SelectionActionsControl from './properties/SelectionActionsControl.vue' import StrokeSection from './properties/StrokeSection.vue' import TypographySection from './properties/TypographySection.vue' import VariablesSection from './properties/VariablesSection.vue' @@ -45,9 +46,7 @@ const { panels } = useI18n() {{ panels.layersCount({ count: String(multiCount) }) }} -
- -
+ @@ -71,6 +70,7 @@ const { panels } = useI18n() node.type }} {{ node.name }} + @@ -99,6 +99,7 @@ const { panels } = useI18n() + diff --git a/src/components/properties/AppearanceSection.vue b/src/components/properties/AppearanceSection.vue index d065fe3f0..a1afcf053 100644 --- a/src/components/properties/AppearanceSection.vue +++ b/src/components/properties/AppearanceSection.vue @@ -1,15 +1,45 @@ -
+
+ + + + +
- + + + + + + + + + +
+import { computed } from 'vue' + +import { useI18n, useMask } from '@open-pencil/vue' + +import AppSelect from '@/components/ui/AppSelect.vue' +import PanelSection from '@/components/ui/PanelSection.vue' +import Tip from '@/components/ui/Tip.vue' + +import type { MaskType } from '@open-pencil/scene-graph' + +const { panels } = useI18n() +const { active, maskType, setMaskType } = useMask() + +const maskTypeOptions = computed>(() => [ + { value: 'ALPHA', label: panels.value.maskTypeAlpha }, + { value: 'VECTOR', label: panels.value.maskTypeVector }, + { value: 'LUMINANCE', label: panels.value.maskTypeLuminance } +]) + +const selectedMaskType = computed({ + get: () => maskType.value, + set: setMaskType +}) + + + diff --git a/src/components/properties/SelectionActionsControl.vue b/src/components/properties/SelectionActionsControl.vue new file mode 100644 index 000000000..cc3c57bcd --- /dev/null +++ b/src/components/properties/SelectionActionsControl.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/components/ui/AppSelect.vue b/src/components/ui/AppSelect.vue index e603aff6f..5c7966f49 100644 --- a/src/components/ui/AppSelect.vue +++ b/src/components/ui/AppSelect.vue @@ -15,6 +15,7 @@ import { useSelectUI } from '@/components/ui/select' import type { SelectUi } from '@/components/ui/select' interface AppSelectUi extends SelectUi { + value?: string viewport?: string indicator?: string } @@ -37,6 +38,7 @@ const select = useSelectUI({ content: ui?.content ?? 'max-h-56', item: ui?.item ?? 'rounded py-1.5 pr-2 pl-6 text-xs' }) +const value = ui?.value ?? 'min-w-0 flex-1 truncate text-left' const viewport = ui?.viewport ?? 'p-0.5' const indicator = ui?.indicator ?? 'absolute left-1.5 inline-flex items-center justify-center' @@ -44,7 +46,7 @@ const indicator = ui?.indicator ?? 'absolute left-1.5 inline-flex items-center j