Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions docs/asset-editor-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ These shortcuts allow you to quickly switch between the tools in the editor.
| **u** | Rectangle tool |
| **c** | Circle tool |
| **m** | Marquee tool |
| **h** | Pan tool |
| **q** | Pan tool |
| **space** | Temporarily enter pan mode (release space to return to previous tool) |
| **alt** | Temporarily enter eyedropper mode (release alt to return to previous tool)

Expand All @@ -40,16 +40,17 @@ These shortcuts are used to perform advanced edit operations on sprites or tilem

Each of these shortcuts are affected by the marquee tool.
If a portion of the asset is selected by the marquee tool, then the shortcut transformation will only apply to the selected area.
If editing an animation, add the **shift** key to the shortcut to affect all frames at once.

| Shortcut | Description |
| -------------- | ----------- |
| **shift + h** | Flip horizontally |
| **shift + v** | Flip vertically |
| **]** | Rotate clockwise |
| **[** | Rotate counterclockwise |
| **Arrow Key** | Move marquee tool selection by one pixel |
| **shift + r** | Replace all instances of selected background color/tile with selected foreground color/tile |
| **backspace** | Delete current marquee tool selection |
| **h** | Flip horizontally |
| **v** | Flip vertically |
| **]** | Rotate clockwise |
| **[** | Rotate counterclockwise |
| **r** | Replace all instances of selected background color/tile with selected foreground color/tile |


## Image/Animation editor-only shortcuts
Expand All @@ -60,4 +61,6 @@ These shortcuts are only available in the image and animation editors (not the t
| ---------------------------------- | ----------- |
| **shift + 1-9** or **shift + a-f** | Outline the current image with the color in the palette corresponding to the selected number. For example, **shift + f** will outline with color number 15 (black) |
| **0-9** | Select a foreground color from the palette (first ten colors only) |
| **.** | Advance forwards one frame in the current animation |
| **,** | Advance backwards one frame in the current animation |

38 changes: 0 additions & 38 deletions webapp/src/components/ImageEditor/ImageCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,34 +291,6 @@ export class ImageCanvasImpl extends React.Component<ImageCanvasProps, {}> imple

this.hasInteracted = true;

if (this.shouldHandleCanvasShortcut() && this.editState?.floating?.image) {
let moved = false;

switch (ev.key) {
case 'ArrowLeft':
this.editState.layerOffsetX = Math.max(this.editState.layerOffsetX - 1, -this.editState.floating.image.width);
moved = true;
break;
case 'ArrowUp':
this.editState.layerOffsetY = Math.max(this.editState.layerOffsetY - 1, -this.editState.floating.image.height);
moved = true;
break;
case 'ArrowRight':
this.editState.layerOffsetX = Math.min(this.editState.layerOffsetX + 1, this.editState.width);
moved = true;
break;
case 'ArrowDown':
this.editState.layerOffsetY = Math.min(this.editState.layerOffsetY + 1, this.editState.height);
moved = true;
break;
}

if (moved) {
this.props.dispatchImageEdit(this.editState.toImageState());
ev.preventDefault();
}
}

if (!ev.repeat) {
// prevent blockly's ctrl+c / ctrl+v handler
if ((ev.ctrlKey || ev.metaKey) && (ev.key === 'c' || ev.key === 'v')) {
Expand All @@ -336,11 +308,6 @@ export class ImageCanvasImpl extends React.Component<ImageCanvasProps, {}> imple
ev.preventDefault();
}

if ((ev.key === "Backspace" || ev.key === "Delete") && this.editState?.floating?.image && this.shouldHandleCanvasShortcut()) {
this.deleteSelection();
ev.preventDefault();
}

// hotkeys for switching temporarily between tools
this.lastTool = this.props.tool;
switch (ev.keyCode) {
Expand Down Expand Up @@ -983,11 +950,6 @@ export class ImageCanvasImpl extends React.Component<ImageCanvasProps, {}> imple
this.props.dispatchImageEdit(this.editState.toImageState());
}

protected deleteSelection() {
this.editState.floating = null;
this.props.dispatchImageEdit(this.editState.toImageState());
}

protected cloneCanvasStyle(base: HTMLCanvasElement, target: HTMLCanvasElement) {
target.style.position = base.style.position;
target.style.width = base.style.width;
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/components/ImageEditor/actions/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ export const dispatchDisableResize = () => ({ type: actions.DISABLE_RESIZE })
export const dispatchChangeAssetName = (name: string) => ({ type: actions.CHANGE_ASSET_NAME, name });

export const dispatchOpenAsset = (asset: pxt.Asset, keepPast: boolean, gallery?: GalleryTile[]) => ({ type: actions.OPEN_ASSET, asset, keepPast, gallery })
export const dispatchSetFrames = (frames: pxt.sprite.ImageState[]) => ({ type: actions.SET_FRAMES, frames });
export const dispatchSetFrames = (frames: pxt.sprite.ImageState[], currentFrame?: number) => ({ type: actions.SET_FRAMES, frames, currentFrame });
201 changes: 191 additions & 10 deletions webapp/src/components/ImageEditor/keyboardShortcuts.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import { Store } from 'redux';
import { ImageEditorTool, ImageEditorStore, TilemapState, AnimationState, CursorSize } from './store/imageReducer';
import { dispatchChangeZoom, dispatchUndoImageEdit, dispatchRedoImageEdit, dispatchChangeImageTool, dispatchSwapBackgroundForeground, dispatchChangeSelectedColor, dispatchImageEdit, dispatchChangeCursorSize} from './actions/dispatch';
import { dispatchChangeZoom, dispatchUndoImageEdit, dispatchRedoImageEdit, dispatchChangeImageTool, dispatchSwapBackgroundForeground, dispatchChangeSelectedColor, dispatchImageEdit, dispatchChangeCursorSize, dispatchChangeCurrentFrame, dispatchSetFrames} from './actions/dispatch';
import { mainStore } from './store/imageStore';
import { EditState, flipEdit, getEditState, outlineEdit, replaceColorEdit, rotateEdit } from './toolDefinitions';
let store = mainStore;
Expand Down Expand Up @@ -60,6 +60,7 @@ function handleUndoRedo(event: KeyboardEvent) {

function overrideBlocklyShortcuts(event: KeyboardEvent) {
if (event.key === "Backspace" || event.key === "Delete") {
handleKeyDown(event);
event.stopPropagation();
}
}
Expand All @@ -78,7 +79,7 @@ function handleKeyDown(event: KeyboardEvent) {
case "e":
setTool(ImageEditorTool.Erase);
break;
case "h":
case "q":
setTool(ImageEditorTool.Pan);
break;
case "b":
Expand Down Expand Up @@ -111,34 +112,68 @@ function handleKeyDown(event: KeyboardEvent) {
case "x":
swapForegroundBackground();
break;
case "H":
case "h":
flip(false);
break;
case "V":
case "v":
flip(true);
break;
case "H":
flipAllFrames(false);
break;
case "V":
flipAllFrames(true);
break;
case "[":
rotate(false);
break;
case "]":
rotate(true);
break;
case "{":
rotateAllFrames(false);
break;
case "}":
rotateAllFrames(true);
break;
case ">":
changeCursorSize(true);
break;
case "<":
changeCursorSize(false);
break;

case ".":
advanceFrame(true);
break;
case ",":
advanceFrame(false);
break;
case "r":
doColorReplace();
break;
case "R":
doColorReplaceAllFrames();
break;
case "ArrowLeft":
moveMarqueeSelection(-1, 0, event.shiftKey);
break;
case "ArrowRight":
moveMarqueeSelection(1, 0, event.shiftKey);
break;
case "ArrowUp":
moveMarqueeSelection(0, -1, event.shiftKey);
break;
case "ArrowDown":
moveMarqueeSelection(0, 1, event.shiftKey);
break;
case "Backspace":
case "Delete":
deleteSelection(event.shiftKey);
break;
}

const editorState = store.getState().editor;

if (event.shiftKey && event.code === "KeyR") {
replaceColor(editorState.backgroundColor, editorState.selectedColor);
return;
}

if (!editorState.isTilemap && /^Digit\d$/.test(event.code)) {
const keyAsNum = +event.code.slice(-1);
const color = keyAsNum + (event.shiftKey ? 9 : 0);
Expand Down Expand Up @@ -216,12 +251,20 @@ export function flip(vertical: boolean) {
dispatchAction(dispatchImageEdit(flipped.toImageState()));
}

export function flipAllFrames(vertical: boolean) {
editAllFrames(() => flip(vertical), editState => flipEdit(editState, vertical, false));
}

export function rotate(clockwise: boolean) {
const [ editState, type ] = currentEditState();
const rotated = rotateEdit(editState, clockwise, type === "tilemap", type === "animation");
dispatchAction(dispatchImageEdit(rotated.toImageState()));
}

export function rotateAllFrames(clockwise: boolean) {
editAllFrames(() => rotate(clockwise), editState => rotateEdit(editState, clockwise, false, true));
}

export function outline(color: number) {
const [ editState, type ] = currentEditState();

Expand All @@ -235,4 +278,142 @@ export function replaceColor(fromColor: number, toColor: number) {
const [ editState, type ] = currentEditState();
const replaced = replaceColorEdit(editState, fromColor, toColor);
dispatchAction(dispatchImageEdit(replaced.toImageState()));
}

function doColorReplace() {
const state = store.getState();

const fromColor = state.editor.backgroundColor;
const toColor = state.editor.selectedColor;

const [ editState ] = currentEditState();
const replaced = replaceColorEdit(editState, fromColor, toColor);
dispatchAction(dispatchImageEdit(replaced.toImageState()));
}

function doColorReplaceAllFrames() {
const state = store.getState();

const fromColor = state.editor.backgroundColor;
const toColor = state.editor.selectedColor;

editAllFrames(doColorReplace, editState => replaceColorEdit(editState, fromColor, toColor))
}

export function advanceFrame(forwards: boolean) {
const state = store.getState();

if (state.editor.isTilemap) return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check if it's the image editor as well?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's what the frames length check does! the image editor uses the same state as the animation editor, it just has only one frame


const present = state.store.present as AnimationState;

if (present.frames.length <= 1) return;

let nextFrame: number;
if (forwards) {
nextFrame = (present.currentFrame + 1) % present.frames.length;
}
else {
nextFrame = (present.currentFrame + present.frames.length - 1) % present.frames.length;
}

dispatchAction(dispatchChangeCurrentFrame(nextFrame));
}

function editAllFrames(singleFrameShortcut: () => void, doEdit: (editState: EditState) => EditState) {
const state = store.getState();

if (state.editor.isTilemap) {
singleFrameShortcut();
return;
}

const present = state.store.present as AnimationState;

if (present.frames.length === 1) {
singleFrameShortcut();
return;
}

const current = present.frames[present.currentFrame];

// if the current frame has a marquee selection, apply that selection
// to all frames
const hasFloatingLayer = !!current.floating;
const layerWidth = current.floating?.bitmap.width;
const layerHeight = current.floating?.bitmap.height;
const layerX = current.layerOffsetX;
const layerY = current.layerOffsetY;

const newFrames: pxt.sprite.ImageState[] = [];

for (const frame of present.frames) {
const editState = getEditState(frame, false);

if (hasFloatingLayer) {
if (editState.floating?.image) {
// check the existing floating layer to see if it matches before
// merging down. otherwise non-square rotations might lose
// information if they cause the floating layer to go off the canvas
if (editState.layerOffsetX !== layerX ||
editState.layerOffsetY !== layerY ||
editState.floating.image.width !== layerWidth ||
editState.floating.image.height !== layerHeight
) {
editState.mergeFloatingLayer();
editState.copyToLayer(layerX, layerY, layerWidth, layerHeight, true);
}
}
else {
editState.copyToLayer(layerX, layerY, layerWidth, layerHeight, true);
}
}
else {
editState.mergeFloatingLayer();
}

const edited = doEdit(editState);
newFrames.push(edited.toImageState());
}

dispatchAction(dispatchSetFrames(newFrames, present.currentFrame));
}

function moveMarqueeSelection(dx: number, dy: number, allFrames = false) {
const [ editState ] = currentEditState();

if (!editState.floating?.image) return;

const moveState = (editState: EditState) => {
editState.layerOffsetX += dx;
editState.layerOffsetY += dy;
return editState;
};


if (!allFrames) {
dispatchAction(dispatchImageEdit(moveState(editState).toImageState()));
}
else {
editAllFrames(() => moveMarqueeSelection(dx, dy), moveState);
}
}

function deleteSelection(allFrames = false) {
const [ editState ] = currentEditState();

if (!editState.floating?.image) return;

const deleteFloatingLayer = (editState: EditState) => {
editState.floating = null;
return editState;
};


if (!allFrames) {
dispatchAction(dispatchImageEdit(deleteFloatingLayer(editState).toImageState()));
}
else {
editAllFrames(() => deleteSelection(), deleteFloatingLayer);
}
}
2 changes: 1 addition & 1 deletion webapp/src/components/ImageEditor/store/imageReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ const animationReducer = (state: AnimationState, action: any): AnimationState =>
return {
...state,
frames: action.frames,
currentFrame: 0
currentFrame: action.currentFrame || 0
};
default:
return state;
Expand Down