diff --git a/packages/drawnix/src/components/icons.tsx b/packages/drawnix/src/components/icons.tsx index fc77e089..e9f186f0 100644 --- a/packages/drawnix/src/components/icons.tsx +++ b/packages/drawnix/src/components/icons.tsx @@ -9,7 +9,7 @@ export const HandIcon = createIcon( - + , ); export const SelectionIcon = createIcon( @@ -17,7 +17,7 @@ export const SelectionIcon = createIcon( - + , ); export const MindIcon = createIcon( @@ -25,7 +25,7 @@ export const MindIcon = createIcon( - + , ); export const ShapeIcon = createIcon( @@ -33,7 +33,7 @@ export const ShapeIcon = createIcon( - + , ); export const TextIcon = createIcon( @@ -41,7 +41,7 @@ export const TextIcon = createIcon( - + , ); export const EraseIcon = createIcon( @@ -57,7 +57,7 @@ export const EraseIcon = createIcon( - + , ); export const StraightArrowLineIcon = createIcon( @@ -69,7 +69,7 @@ export const StraightArrowLineIcon = createIcon( transform="translate(8.500035, 7.500035) rotate(-135.000000) translate(-8.500035, -7.500035) " /> - + , ); export const RectangleIcon = createIcon( @@ -80,7 +80,7 @@ export const RectangleIcon = createIcon( strokeWidth="2" fill="none" > - + , ); export const TerminalIcon = createIcon( @@ -88,7 +88,7 @@ export const TerminalIcon = createIcon( - + , ); export const EllipseIcon = createIcon( @@ -96,7 +96,7 @@ export const EllipseIcon = createIcon( - + , ); export const TriangleIcon = createIcon( @@ -104,7 +104,7 @@ export const TriangleIcon = createIcon( - + , ); export const DiamondIcon = createIcon( @@ -115,7 +115,7 @@ export const DiamondIcon = createIcon( transform="translate(7.989647, 8.003560) rotate(-315.000000) translate(-7.989647, -8.003560) " /> - + , ); export const ParallelogramIcon = createIcon( @@ -123,7 +123,7 @@ export const ParallelogramIcon = createIcon( - + , ); export const RoundRectangleIcon = createIcon( @@ -131,7 +131,7 @@ export const RoundRectangleIcon = createIcon( - + , ); export const StraightArrowIcon = createIcon( @@ -142,7 +142,7 @@ export const StraightArrowIcon = createIcon( transform="translate(8.500035, 7.500035) rotate(-135.000000) translate(-8.500035, -7.500035) " /> - + , ); export const ElbowArrowIcon = createIcon( @@ -150,7 +150,7 @@ export const ElbowArrowIcon = createIcon( - + , ); export const CurveArrowIcon = createIcon( @@ -158,7 +158,7 @@ export const CurveArrowIcon = createIcon( - + , ); export const MenuIcon = createIcon( @@ -177,7 +177,7 @@ export const MenuIcon = createIcon( - + , ); export const GithubIcon = createIcon( @@ -190,7 +190,7 @@ export const GithubIcon = createIcon( d="M7.5 15.833c-3.583 1.167-3.583-2.083-5-2.5m10 4.167v-2.917c0-.833.083-1.166-.417-1.666 2.334-.25 4.584-1.167 4.584-5a3.833 3.833 0 0 0-1.084-2.667 3.5 3.5 0 0 0-.083-2.667s-.917-.25-2.917 1.084a10.25 10.25 0 0 0-5.166 0C5.417 2.333 4.5 2.583 4.5 2.583a3.5 3.5 0 0 0-.083 2.667 3.833 3.833 0 0 0-1.084 2.667c0 3.833 2.25 4.75 4.584 5-.5.5-.5 1-.417 1.666V17.5" strokeWidth="1.25" > - + , ); export const ExportImageIcon = createIcon( @@ -210,7 +210,7 @@ export const ExportImageIcon = createIcon( - + , ); export const ZoomOutIcon = createIcon( @@ -221,7 +221,7 @@ export const ZoomOutIcon = createIcon( d="M6.85,2.73225886e-13 C10.6331505,2.73225886e-13 13.7,3.06684946 13.7,6.85 C13.7,8.54194045 13.0865836,10.0906098 12.0700142,11.2857448 L15.4201976,14.5717081 C15.6567367,14.8037768 15.6603607,15.1836585 15.4282919,15.4201976 C15.1962232,15.6567367 14.8163415,15.6603607 14.5798024,15.4282919 L14.5798024,15.4282919 L11.2163456,12.128262 C10.0309427,13.1099691 8.50937591,13.7 6.85,13.7 C3.06684946,13.7 4.58522109e-14,10.6331505 4.58522109e-14,6.85 C4.58522109e-14,3.06684946 3.06684946,2.73225886e-13 6.85,2.73225886e-13 Z M6.85,1.2 C3.72959116,1.2 1.2,3.72959116 1.2,6.85 C1.2,9.97040884 3.72959116,12.5 6.85,12.5 C8.31753357,12.5 9.65438791,11.9404957 10.6588859,11.0231643 C10.6855412,10.9625408 10.7245275,10.9050898 10.7743982,10.8542584 C10.8288931,10.7987137 10.8915387,10.7560124 10.9585649,10.7261903 C11.9144009,9.71595758 12.5,8.35136579 12.5,6.85 C12.5,3.72959116 9.97040884,1.2 6.85,1.2 Z M4.6,6.2 L9.12944565,6.2 C9.4608165,6.2 9.72944565,6.46862915 9.72944565,6.8 C9.72944565,7.09823376 9.51185604,7.34564675 9.22676876,7.39214701 L9.12944565,7.4 L4.6,7.4 C4.26862915,7.4 4,7.13137085 4,6.8 C4,6.50176624 4.21758961,6.25435325 4.50267688,6.20785299 L4.6,6.2 L9.12944565,6.2 Z" > - + , ); export const ZoomInIcon = createIcon( @@ -232,7 +232,7 @@ export const ZoomInIcon = createIcon( d="M6.85,-1.81188398e-13 C10.6331505,-1.81188398e-13 13.7,3.06684946 13.7,6.85 C13.7,8.54194045 13.0865836,10.0906098 12.0700142,11.2857448 L15.4201976,14.5717081 C15.6567367,14.8037768 15.6603607,15.1836585 15.4282919,15.4201976 C15.1962232,15.6567367 14.8163415,15.6603607 14.5798024,15.4282919 L14.5798024,15.4282919 L11.2163456,12.128262 C10.0309427,13.1099691 8.50937591,13.7 6.85,13.7 C3.06684946,13.7 4.61852778e-14,10.6331505 4.61852778e-14,6.85 C4.61852778e-14,3.06684946 3.06684946,-1.81188398e-13 6.85,-1.81188398e-13 Z M6.85,1.2 C3.72959116,1.2 1.2,3.72959116 1.2,6.85 C1.2,9.97040884 3.72959116,12.5 6.85,12.5 C8.31753357,12.5 9.65438791,11.9404957 10.6588859,11.0231643 C10.6855412,10.9625408 10.7245275,10.9050898 10.7743982,10.8542584 C10.8288931,10.7987137 10.8915387,10.7560124 10.9585649,10.7261903 C11.9144009,9.71595758 12.5,8.35136579 12.5,6.85 C12.5,3.72959116 9.97040884,1.2 6.85,1.2 Z M6.86472282,3.93527718 C7.16295659,3.93527718 7.41036958,4.15286679 7.45686984,4.43795406 L7.46472282,4.53527718 L7.464,6.19927718 L9.12944565,6.2 C9.42767941,6.2 9.6750924,6.41758961 9.72159266,6.70267688 L9.72944565,6.8 C9.72944565,7.09823376 9.51185604,7.34564675 9.22676876,7.39214701 L9.12944565,7.4 L7.464,7.39927718 L7.46472282,9.06472282 C7.46472282,9.36295659 7.24713321,9.61036958 6.96204594,9.65686984 L6.86472282,9.66472282 C6.56648906,9.66472282 6.31907607,9.44713321 6.27257581,9.16204594 L6.26472282,9.06472282 L6.264,7.39927718 L4.6,7.4 C4.30176624,7.4 4.05435325,7.18241039 4.00785299,6.89732312 L4,6.8 C4,6.50176624 4.21758961,6.25435325 4.50267688,6.20785299 L4.6,6.2 L6.264,6.19927718 L6.26472282,4.53527718 C6.26472282,4.2701805 6.43664548,4.0452385 6.67507642,3.96586557 L6.76739971,3.94313016 L6.86472282,3.93527718 Z" > - + , ); export const SaveFileIcon = createIcon( @@ -243,7 +243,7 @@ export const SaveFileIcon = createIcon( d="M11.064 9.1l2.645 2.595.03-.029.848.849-3.523 3.323-.848-.848 1.994-1.883H7.5v-1.2h4.712l-1.996-1.958.848-.849zM9.356.3L13.7 3.71V7.9h-1.2l-.001-2.633H8.5V1.5L3.1 1.5a.4.4 0 0 0-.392.32L2.7 1.9v12a.4.4 0 0 0 .32.392l.08.008h3.418v1.2H3.1a1.6 1.6 0 0 1-1.593-1.454L1.5 13.9v-12A1.6 1.6 0 0 1 2.954.307L3.1.3h6.256zM9.7 2.095v1.973l2.51-.001L9.7 2.095z" > - + , ); export const OpenFileIcon = createIcon( @@ -254,7 +254,7 @@ export const OpenFileIcon = createIcon( strokeWidth="1.25" /> - + , ); export const BackgroundColorIcon = createIcon( @@ -270,7 +270,7 @@ export const BackgroundColorIcon = createIcon( fillOpacity=".12" > - + , ); export const NoColorIcon = createIcon( @@ -284,7 +284,7 @@ export const NoColorIcon = createIcon( - + , ); export const Check = createIcon( @@ -299,7 +299,7 @@ export const Check = createIcon( strokeLinejoin="round" > - + , ); export const StrokeIcon = createIcon( @@ -320,7 +320,7 @@ export const StrokeIcon = createIcon( fillOpacity=".12" > - + , ); export const StrokeWhiteIcon = createIcon( @@ -347,7 +347,7 @@ export const StrokeWhiteIcon = createIcon( /> - + , ); export const StrokeStyleNormalIcon = createIcon( @@ -356,7 +356,7 @@ export const StrokeStyleNormalIcon = createIcon( - + , ); export const StrokeStyleDashedIcon = createIcon( @@ -366,7 +366,7 @@ export const StrokeStyleDashedIcon = createIcon( - + , ); export const StrokeStyleDotedIcon = createIcon( @@ -381,7 +381,7 @@ export const StrokeStyleDotedIcon = createIcon( - + , ); export const FontColorIcon: React.FC<{ currentColor?: string }> = ({ @@ -424,7 +424,7 @@ export const UndoIcon = createIcon( > - + , ); export const RedoIcon = createIcon( @@ -437,7 +437,7 @@ export const RedoIcon = createIcon( > - + , ); export const TrashIcon = createIcon( @@ -446,7 +446,7 @@ export const TrashIcon = createIcon( strokeWidth="1.25" d="M3.333 5.833h13.334M8.333 9.167v5M11.667 9.167v5M4.167 5.833l.833 10c0 .92.746 1.667 1.667 1.667h6.666c.92 0 1.667-.746 1.667-1.667l.833-10M7.5 5.833v-2.5c0-.46.373-.833.833-.833h3.334c.46 0 .833.373.833.833v2.5" > - + , ); export const DuplicateIcon = createIcon( @@ -460,7 +460,7 @@ export const DuplicateIcon = createIcon( - + , ); export const FeltTipPenIcon = createIcon( @@ -470,7 +470,7 @@ export const FeltTipPenIcon = createIcon( xmlns="http://www.w3.org/2000/svg" > - + , ); export const ImageIcon = createIcon( @@ -478,7 +478,7 @@ export const ImageIcon = createIcon( - + , ); export const ExtraToolsIcon = createIcon( @@ -493,7 +493,7 @@ export const ExtraToolsIcon = createIcon( - + , ); export const MermaidLogoIcon = createIcon( @@ -507,7 +507,7 @@ export const MermaidLogoIcon = createIcon( fill="currentColor" d="M407.48,111.18C335.587,108.103 269.573,152.338 245.08,220C220.587,152.338 154.573,108.103 82.68,111.18C80.285,168.229 107.577,222.632 154.74,254.82C178.908,271.419 193.35,298.951 193.27,328.27L193.27,379.13L296.9,379.13L296.9,328.27C296.816,298.953 311.255,271.42 335.42,254.82C382.596,222.644 409.892,168.233 407.48,111.18Z" /> - + , ); export const MarkdownLogoIcon = createIcon( @@ -515,7 +515,7 @@ export const MarkdownLogoIcon = createIcon( - + , ); export const LinkIcon = createIcon( @@ -526,16 +526,15 @@ export const LinkIcon = createIcon( transform="rotate(46 8.253 8.13)" > - + , ); - export const ArrowIcon = createIcon( - + - + , ); export const LineIcon = createIcon( @@ -543,7 +542,7 @@ export const LineIcon = createIcon( - + , ); export const StraightLineIcon = createIcon( @@ -551,5 +550,5 @@ export const StraightLineIcon = createIcon( - + , ); diff --git a/packages/drawnix/src/components/toolbar/app-toolbar/app-menu-items.tsx b/packages/drawnix/src/components/toolbar/app-toolbar/app-menu-items.tsx index 1d6c0df5..ebdb4bb7 100644 --- a/packages/drawnix/src/components/toolbar/app-toolbar/app-menu-items.tsx +++ b/packages/drawnix/src/components/toolbar/app-toolbar/app-menu-items.tsx @@ -1,4 +1,5 @@ import { + DuplicateIcon, ExportImageIcon, GithubIcon, OpenFileIcon, @@ -17,7 +18,7 @@ import { import { loadFromJSON, saveAsJSON } from '../../../data/json'; import MenuItem from '../../menu/menu-item'; import MenuItemLink from '../../menu/menu-item-link'; -import { saveAsImage } from '../../../utils/image'; +import { copyAsImage, saveAsImage } from '../../../utils/image'; import { useDrawnix } from '../../../hooks/use-drawnix'; import { useI18n } from '../../../i18n'; import Menu from '../../menu/menu'; @@ -38,7 +39,9 @@ export const SaveToFile = () => { icon={SaveFileIcon} aria-label={t('menu.saveFile')} shortcut={getShortcutKey('CtrlOrCmd+S')} - >{t('menu.saveFile')} + > + {t('menu.saveFile')} + ); }; SaveToFile.displayName = 'SaveToFile'; @@ -50,7 +53,7 @@ export const OpenFile = () => { const clearAndLoad = ( value: PlaitElement[], viewport?: Viewport, - theme?: PlaitTheme + theme?: PlaitTheme, ) => { board.children = value; board.viewport = viewport || { zoom: 1 }; @@ -72,7 +75,9 @@ export const OpenFile = () => { }} icon={OpenFileIcon} aria-label={t('menu.open')} - >{t('menu.open')} + > + {t('menu.open')} + ); }; OpenFile.displayName = 'OpenFile'; @@ -89,13 +94,15 @@ export const SaveAsImage = () => { saveAsImage(board, true); }} submenu={ - { - const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, { - bubbles: true, - cancelable: true, - }); - menuContentProps.onSelect?.(itemSelectEvent); - }}> + { + const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, { + bubbles: true, + cancelable: true, + }); + menuContentProps.onSelect?.(itemSelectEvent); + }} + > { saveAsImage(board, true); @@ -145,6 +152,53 @@ export const CleanBoard = () => { }; CleanBoard.displayName = 'CleanBoard'; +export const CopyAsImage = () => { + const board = useBoard(); + const menuContentProps = useContext(MenuContentPropsContext); + const { t } = useI18n(); + return ( + { + copyAsImage(board, true); + }} + submenu={ + { + const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, { + bubbles: true, + cancelable: true, + }); + menuContentProps.onSelect?.(itemSelectEvent); + }} + > + { + copyAsImage(board, true); + }} + aria-label={t('menu.exportImage.png')} + > + {t('menu.exportImage.png')} + + { + copyAsImage(board, false); + }} + aria-label={t('menu.exportImage.jpg')} + > + {t('menu.exportImage.jpg')} + + + } + shortcut={getShortcutKey('CtrlOrCmd+Shift+C')} + aria-label={t('menu.copyImage')} + > + {t('menu.copyImage')} + + ); +}; +CopyAsImage.displayName = 'CopyAsImage'; export const Socials = () => { return ( { + @@ -129,7 +137,6 @@ export const AppToolbar = () => { }} /> )} - ); diff --git a/packages/drawnix/src/i18n/translations/zh.ts b/packages/drawnix/src/i18n/translations/zh.ts index 1c94fef1..b800d7e2 100644 --- a/packages/drawnix/src/i18n/translations/zh.ts +++ b/packages/drawnix/src/i18n/translations/zh.ts @@ -71,11 +71,12 @@ const zhTranslations: Translations = { 'language.english': 'English', 'language.russian': 'Русский', 'language.arabic': 'عربي', - + // Menu items 'menu.open': '打开', 'menu.saveFile': '保存文件', 'menu.exportImage': '导出图片', + 'menu.copyImage': '复制图片', 'menu.exportImage.png': 'PNG', 'menu.exportImage.jpg': 'JPG', 'menu.cleanBoard': '清除画布', @@ -117,7 +118,7 @@ const zhTranslations: Translations = { 'popupToolbar.fontColor': '字体颜色', 'popupToolbar.link': '链接', 'popupToolbar.stroke': '边框', - + // Text placeholders 'textPlaceholders.link': '链接', 'textPlaceholders.text': '文本', @@ -136,7 +137,7 @@ const zhTranslations: Translations = { // Draw elements text 'draw.lineText': '文本', 'draw.geometryText': '文本', - + // Mind map elements text 'mind.centralText': '中心主题', 'mind.abstractNodeText': '摘要', @@ -168,7 +169,6 @@ const zhTranslations: Translations = { 'tutorial.appToolbar': '导出,语言设置,...', 'tutorial.creationToolbar': '选择一个工具开始你的创作', 'tutorial.themeDescription': '在明亮和黑暗主题之间切换', - }; -export default zhTranslations; \ No newline at end of file +export default zhTranslations; diff --git a/packages/drawnix/src/i18n/types.ts b/packages/drawnix/src/i18n/types.ts index 2d300243..9e68ccfa 100644 --- a/packages/drawnix/src/i18n/types.ts +++ b/packages/drawnix/src/i18n/types.ts @@ -81,6 +81,7 @@ export interface Translations { 'menu.saveFile': string; 'menu.exportImage': string; 'menu.exportImage.png': string; + 'menu.copyImage': string; 'menu.exportImage.jpg': string; 'menu.cleanBoard': string; 'menu.github': string; diff --git a/packages/drawnix/src/plugins/with-hotkey.ts b/packages/drawnix/src/plugins/with-hotkey.ts index 2a61395c..7dd39a58 100644 --- a/packages/drawnix/src/plugins/with-hotkey.ts +++ b/packages/drawnix/src/plugins/with-hotkey.ts @@ -5,7 +5,7 @@ import { PlaitPointerType, } from '@plait/core'; import { isHotkey } from 'is-hotkey'; -import { addImage, saveAsImage } from '../utils/image'; +import { addImage, copyAsImage, saveAsImage } from '../utils/image'; import { saveAsJSON } from '../data/json'; import { DrawnixState } from '../hooks/use-drawnix'; import { BoardCreationMode, setCreationMode } from '@plait/common'; @@ -14,7 +14,7 @@ import { FreehandShape } from './freehand/type'; import { ArrowLineShape, BasicShapes } from '@plait/draw'; export const buildDrawnixHotkeyPlugin = ( - updateAppState: (appState: Partial) => void + updateAppState: (appState: Partial) => void, ) => { const withDrawnixHotkey = (board: PlaitBoard) => { const { globalKeyDown, keyDown } = board; @@ -33,6 +33,11 @@ export const buildDrawnixHotkeyPlugin = ( event.preventDefault(); return; } + if (isHotkey(['mod+shift+c'], { byKey: true })(event)) { + copyAsImage(board, true); + event.preventDefault(); + return; + } if (isHotkey(['mod+s'], { byKey: true })(event)) { saveAsJSON(board); event.preventDefault(); @@ -63,7 +68,7 @@ export const buildDrawnixHotkeyPlugin = ( if (event.key === 'v') { BoardTransforms.updatePointerType( board, - PlaitPointerType.selection + PlaitPointerType.selection, ); updateAppState({ pointer: PlaitPointerType.selection }); event.preventDefault(); diff --git a/packages/drawnix/src/utils/image.ts b/packages/drawnix/src/utils/image.ts index e2ddc0a4..bdda088d 100644 --- a/packages/drawnix/src/utils/image.ts +++ b/packages/drawnix/src/utils/image.ts @@ -19,11 +19,27 @@ export const saveAsImage = (board: PlaitBoard, isTransparent: boolean) => { }); }; +export const copyAsImage = (board: PlaitBoard, isTransparent: boolean) => { + const selectedElements = getSelectedElements(board); + boardToImage(board, { + elements: selectedElements.length > 0 ? selectedElements : undefined, + fillStyle: isTransparent ? 'transparent' : 'white', + }).then(async (image) => { + if (image) { + const pngImage = base64ToBlob(image); + const item = new ClipboardItem({ 'image/png': pngImage }); + + if (!item) return; + await navigator.clipboard.write([item]); + } + }); +}; + export const addImage = async (board: PlaitBoard) => { const imageFile = await fileOpen({ description: 'Image', extensions: Object.keys( - IMAGE_MIME_TYPES + IMAGE_MIME_TYPES, ) as (keyof typeof IMAGE_MIME_TYPES)[], }); insertImage(board, imageFile);