Comparing version 0.1.9 to 0.1.10
@@ -0,1 +1,7 @@ | ||
# 0.1.10 | ||
* Keyboard shortcuts for the selection tool. | ||
* Scroll the selection into view while moving it with the keyboard/mouse. | ||
* Fix toolbar buttons not activating when focused and enter/space is pressed. | ||
* Partial Spanish localization. | ||
# 0.1.9 | ||
@@ -2,0 +8,0 @@ * Fix regression -- color picker hides just after clicking it. |
import '../styles'; | ||
import Editor from '../Editor'; | ||
import getLocalizationTable from '../localizations/getLocalizationTable'; | ||
export default Editor; | ||
export { Editor }; | ||
export { Editor, getLocalizationTable }; |
// Main entrypoint for Webpack when building a bundle for release. | ||
import '../styles'; | ||
import Editor from '../Editor'; | ||
import getLocalizationTable from '../localizations/getLocalizationTable'; | ||
export default Editor; | ||
export { Editor }; | ||
export { Editor, getLocalizationTable }; |
@@ -40,2 +40,3 @@ import EditorImage from './EditorImage'; | ||
private registerListeners; | ||
handleKeyEventsFrom(elem: HTMLElement): void; | ||
dispatch(command: Command, addToHistory?: boolean): void; | ||
@@ -42,0 +43,0 @@ dispatchNoAnnounce(command: Command, addToHistory?: boolean): void; |
@@ -26,7 +26,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
import Mat33 from './geometry/Mat33'; | ||
import { defaultEditorLocalization } from './localization'; | ||
import getLocalizationTable from './localizations/getLocalizationTable'; | ||
export class Editor { | ||
constructor(parent, settings = {}) { | ||
var _a, _b; | ||
this.localization = defaultEditorLocalization; | ||
this.announceUndoCallback = (command) => { | ||
@@ -39,3 +38,3 @@ this.announceForAccessibility(this.localization.undoAnnouncement(command.description(this.localization))); | ||
this.rerenderQueued = false; | ||
this.localization = Object.assign(Object.assign({}, this.localization), settings.localization); | ||
this.localization = Object.assign(Object.assign({}, getLocalizationTable()), settings.localization); | ||
// Fill default settings. | ||
@@ -181,14 +180,3 @@ this.settings = { | ||
}); | ||
this.renderingRegion.addEventListener('keydown', evt => { | ||
if (this.toolController.dispatchInputEvent({ | ||
kind: InputEvtType.KeyPressEvent, | ||
key: evt.key, | ||
ctrlKey: evt.ctrlKey, | ||
})) { | ||
evt.preventDefault(); | ||
} | ||
else if (evt.key === 'Escape') { | ||
this.renderingRegion.blur(); | ||
} | ||
}); | ||
this.handleKeyEventsFrom(this.renderingRegion); | ||
this.container.addEventListener('wheel', evt => { | ||
@@ -240,2 +228,27 @@ let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ); | ||
} | ||
// Adds event listners for keypresses to [elem] and forwards those events to the | ||
// editor. | ||
handleKeyEventsFrom(elem) { | ||
elem.addEventListener('keydown', evt => { | ||
if (this.toolController.dispatchInputEvent({ | ||
kind: InputEvtType.KeyPressEvent, | ||
key: evt.key, | ||
ctrlKey: evt.ctrlKey, | ||
})) { | ||
evt.preventDefault(); | ||
} | ||
else if (evt.key === 'Escape') { | ||
this.renderingRegion.blur(); | ||
} | ||
}); | ||
elem.addEventListener('keyup', evt => { | ||
if (this.toolController.dispatchInputEvent({ | ||
kind: InputEvtType.KeyUpEvent, | ||
key: evt.key, | ||
ctrlKey: evt.ctrlKey, | ||
})) { | ||
evt.preventDefault(); | ||
} | ||
}); | ||
} | ||
// Adds to history by default | ||
@@ -242,0 +255,0 @@ dispatch(command, addToHistory = true) { |
@@ -18,3 +18,4 @@ import Rect2 from './Rect2'; | ||
intersection(other: LineSegment2): IntersectionResult | null; | ||
closestPointTo(target: Point2): import("./Vec3").default; | ||
} | ||
export {}; |
@@ -100,2 +100,18 @@ import Rect2 from './Rect2'; | ||
} | ||
// Returns the closest point on this to [target] | ||
closestPointTo(target) { | ||
// Distance from P1 along this' direction. | ||
const projectedDistFromP1 = target.minus(this.p1).dot(this.direction); | ||
const projectedDistFromP2 = this.length - projectedDistFromP1; | ||
const projection = this.p1.plus(this.direction.times(projectedDistFromP1)); | ||
if (projectedDistFromP1 > 0 && projectedDistFromP1 < this.length) { | ||
return projection; | ||
} | ||
if (Math.abs(projectedDistFromP2) < Math.abs(projectedDistFromP1)) { | ||
return this.p2; | ||
} | ||
else { | ||
return this.p1; | ||
} | ||
} | ||
} |
@@ -33,2 +33,3 @@ import LineSegment2 from './LineSegment2'; | ||
grownBy(margin: number): Rect2; | ||
getClosestPointOnBoundaryTo(target: Point2): import("./Vec3").default; | ||
get corners(): Point2[]; | ||
@@ -35,0 +36,0 @@ get maxDimension(): number; |
@@ -28,2 +28,3 @@ import LineSegment2 from './LineSegment2'; | ||
} | ||
// Returns a copy of this with the given size (but same top-left). | ||
resizedTo(size) { | ||
@@ -113,2 +114,17 @@ return new Rect2(this.x, this.y, size.x, size.y); | ||
} | ||
getClosestPointOnBoundaryTo(target) { | ||
const closestEdgePoints = this.getEdges().map(edge => { | ||
return edge.closestPointTo(target); | ||
}); | ||
let closest = null; | ||
let closestDist = null; | ||
for (const point of closestEdgePoints) { | ||
const dist = point.minus(target).length(); | ||
if (closestDist === null || dist < closestDist) { | ||
closest = point; | ||
closestDist = dist; | ||
} | ||
} | ||
return closest; | ||
} | ||
get corners() { | ||
@@ -115,0 +131,0 @@ return [ |
@@ -111,3 +111,3 @@ import { ToolType } from '../tools/ToolController'; | ||
const undoButton = this.addActionButton({ | ||
label: 'Undo', | ||
label: this.localizationTable.undo, | ||
icon: makeUndoIcon() | ||
@@ -118,3 +118,3 @@ }, () => { | ||
const redoButton = this.addActionButton({ | ||
label: 'Redo', | ||
label: this.localizationTable.redo, | ||
icon: makeRedoIcon(), | ||
@@ -162,7 +162,5 @@ }, () => { | ||
} | ||
for (const tool of toolController.getMatchingTools(ToolType.PanZoom)) { | ||
if (!(tool instanceof PanZoom)) { | ||
throw new Error('All SelectionTools must have kind === ToolType.PanZoom'); | ||
} | ||
(new HandToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container); | ||
const panZoomTool = toolController.getMatchingTools(ToolType.PanZoom)[0]; | ||
if (panZoomTool && panZoomTool instanceof PanZoom) { | ||
(new HandToolWidget(this.editor, panZoomTool, this.localizationTable)).addTo(this.container); | ||
} | ||
@@ -169,0 +167,0 @@ this.setupColorPickers(); |
@@ -6,7 +6,7 @@ import EventDispatcher from '../EventDispatcher'; | ||
const svgNamespace = 'http://www.w3.org/2000/svg'; | ||
const primaryForegroundFill = ` | ||
style='fill: var(--primary-foreground-color);' | ||
const iconColorFill = ` | ||
style='fill: var(--icon-color);' | ||
`; | ||
const primaryForegroundStrokeFill = ` | ||
style='fill: var(--primary-foreground-color); stroke: var(--primary-foreground-color);' | ||
const iconColorStrokeFill = ` | ||
style='fill: var(--icon-color); stroke: var(--icon-color);' | ||
`; | ||
@@ -35,3 +35,3 @@ const checkerboardPatternDef = ` | ||
.toolbar-svg-undo-redo-icon { | ||
stroke: var(--primary-foreground-color); | ||
stroke: var(--icon-color); | ||
stroke-width: 12; | ||
@@ -59,3 +59,3 @@ stroke-linejoin: round; | ||
d='M5,10 L50,90 L95,10 Z' | ||
${primaryForegroundFill} | ||
${iconColorFill} | ||
/> | ||
@@ -75,3 +75,3 @@ </g> | ||
x=10 y=10 width=80 height=50 | ||
${primaryForegroundFill} | ||
${iconColorFill} | ||
/> | ||
@@ -123,3 +123,3 @@ </g> | ||
style=' | ||
stroke: var(--primary-foreground-color); | ||
stroke: var(--icon-color); | ||
stroke-width: 2; | ||
@@ -166,3 +166,3 @@ ' | ||
style=' | ||
stroke: var(--primary-foreground-color); | ||
stroke: var(--icon-color); | ||
stroke-width: 2; | ||
@@ -231,3 +231,3 @@ ' | ||
style=' | ||
stroke: var(--primary-foreground-color); | ||
stroke: var(--icon-color); | ||
stroke-width: 2; | ||
@@ -251,3 +251,3 @@ ' | ||
textNode.style.fontSize = '55px'; | ||
textNode.style.fill = 'var(--primary-foreground-color)'; | ||
textNode.style.fill = 'var(--icon-color)'; | ||
textNode.style.fontFamily = 'monospace'; | ||
@@ -293,3 +293,3 @@ icon.appendChild(textNode); | ||
d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z' | ||
${primaryForegroundStrokeFill} | ||
${iconColorStrokeFill} | ||
/> | ||
@@ -360,3 +360,3 @@ </g> | ||
`); | ||
pipette.style.fill = 'var(--primary-foreground-color)'; | ||
pipette.style.fill = 'var(--icon-color)'; | ||
if (color) { | ||
@@ -363,0 +363,0 @@ const defs = document.createElementNS(svgNamespace, 'defs'); |
@@ -24,2 +24,3 @@ export interface ToolbarLocalization { | ||
zoom: string; | ||
selectionToolKeyboardShortcuts: string; | ||
dropdownShown: (toolName: string) => string; | ||
@@ -26,0 +27,0 @@ dropdownHidden: (toolName: string) => string; |
@@ -17,2 +17,3 @@ export const defaultToolbarLocalization = { | ||
pickColorFronScreen: 'Pick color from screen', | ||
selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.', | ||
touchPanning: 'Touchscreen panning', | ||
@@ -19,0 +20,0 @@ anyDevicePanning: 'Any device panning', |
@@ -13,2 +13,3 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { | ||
var _BaseWidget_hasDropdown; | ||
import { InputEvtType } from '../../types'; | ||
import { toolbarCSSPrefix } from '../HTMLToolbar'; | ||
@@ -48,2 +49,30 @@ import { makeDropdownIcon } from '../icons'; | ||
setupActionBtnClickListener(button) { | ||
const clickTriggers = { enter: true, ' ': true, }; | ||
button.onkeydown = (evt) => { | ||
let handled = false; | ||
if (evt.key in clickTriggers) { | ||
if (!this.disabled) { | ||
this.handleClick(); | ||
handled = true; | ||
} | ||
} | ||
// If we didn't do anything with the event, send it to the editor. | ||
if (!handled) { | ||
this.editor.toolController.dispatchInputEvent({ | ||
kind: InputEvtType.KeyPressEvent, | ||
key: evt.key, | ||
ctrlKey: evt.ctrlKey, | ||
}); | ||
} | ||
}; | ||
button.onkeyup = evt => { | ||
if (evt.key in clickTriggers) { | ||
return; | ||
} | ||
this.editor.toolController.dispatchInputEvent({ | ||
kind: InputEvtType.KeyUpEvent, | ||
key: evt.key, | ||
ctrlKey: evt.ctrlKey, | ||
}); | ||
}; | ||
button.onclick = () => { | ||
@@ -50,0 +79,0 @@ if (!this.disabled) { |
@@ -1,2 +0,2 @@ | ||
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, KeyPressEvent } from '../types'; | ||
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, KeyPressEvent, KeyUpEvent } from '../types'; | ||
import { ToolType } from './ToolController'; | ||
@@ -17,2 +17,3 @@ import ToolEnabledGroup from './ToolEnabledGroup'; | ||
onKeyPress(_event: KeyPressEvent): boolean; | ||
onKeyUp(_event: KeyUpEvent): boolean; | ||
setEnabled(enabled: boolean): void; | ||
@@ -19,0 +20,0 @@ isEnabled(): boolean; |
@@ -19,2 +19,5 @@ import { EditorEventType } from '../types'; | ||
} | ||
onKeyUp(_event) { | ||
return false; | ||
} | ||
setEnabled(enabled) { | ||
@@ -21,0 +24,0 @@ var _a; |
export interface ToolLocalization { | ||
keyboardPanZoom: string; | ||
penTool: (penId: number) => string; | ||
@@ -3,0 +4,0 @@ selectionTool: string; |
@@ -10,2 +10,3 @@ export const defaultToolLocalization = { | ||
pipetteTool: 'Pick color from screen', | ||
keyboardPanZoom: 'Keyboard pan/zoom shortcuts', | ||
textTool: 'Text', | ||
@@ -12,0 +13,0 @@ enterTextToInsert: 'Text to insert', |
@@ -17,3 +17,4 @@ import { Editor } from '../Editor'; | ||
RightClickDrags = 4, | ||
SinglePointerGestures = 8 | ||
SinglePointerGestures = 8, | ||
Keyboard = 16 | ||
} | ||
@@ -20,0 +21,0 @@ export default class PanZoom extends BaseTool { |
@@ -15,2 +15,3 @@ import Mat33 from '../geometry/Mat33'; | ||
PanZoomMode[PanZoomMode["SinglePointerGestures"] = 8] = "SinglePointerGestures"; | ||
PanZoomMode[PanZoomMode["Keyboard"] = 16] = "Keyboard"; | ||
})(PanZoomMode || (PanZoomMode = {})); | ||
@@ -137,2 +138,5 @@ export default class PanZoom extends BaseTool { | ||
onKeyPress({ key }) { | ||
if (!(this.mode & PanZoomMode.Keyboard)) { | ||
return false; | ||
} | ||
let translation = Vec2.zero; | ||
@@ -139,0 +143,0 @@ let scale = 1; |
import Command from '../commands/Command'; | ||
import Editor from '../Editor'; | ||
import Mat33 from '../geometry/Mat33'; | ||
import Rect2 from '../geometry/Rect2'; | ||
import { Point2, Vec2 } from '../geometry/Vec2'; | ||
import { PointerEvt } from '../types'; | ||
import { KeyPressEvent, KeyUpEvent, PointerEvt } from '../types'; | ||
import BaseTool from './BaseTool'; | ||
@@ -23,3 +24,4 @@ import { ToolType } from './ToolController'; | ||
private computeTransformCommands; | ||
finishDragging(): void; | ||
transformPreview(transform: Mat33): void; | ||
finalizeTransform(): void; | ||
private static ApplyTransformationCommand; | ||
@@ -36,2 +38,3 @@ private previewTransformCmds; | ||
updateUI(): void; | ||
scrollTo(): void; | ||
deleteSelectedObjects(): Command; | ||
@@ -50,4 +53,8 @@ duplicateSelectedObjects(): Command; | ||
private onGestureEnd; | ||
private zoomToSelection; | ||
onPointerUp(event: PointerEvt): void; | ||
onGestureCancel(): void; | ||
private static handleableKeys; | ||
onKeyPress(event: KeyPressEvent): boolean; | ||
onKeyUp(evt: KeyUpEvent): boolean; | ||
setEnabled(enabled: boolean): void; | ||
@@ -54,0 +61,0 @@ getSelection(): Selection | null; |
@@ -18,2 +18,3 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
import { EditorEventType } from '../types'; | ||
import Viewport from '../Viewport'; | ||
import BaseTool from './BaseTool'; | ||
@@ -147,9 +148,9 @@ import { ToolType } from './ToolController'; | ||
this.handleBackgroundDrag(deltaPosition); | ||
}, () => this.finishDragging()); | ||
}, () => this.finalizeTransform()); | ||
makeDraggable(resizeCorner, (deltaPosition) => { | ||
this.handleResizeCornerDrag(deltaPosition); | ||
}, () => this.finishDragging()); | ||
}, () => this.finalizeTransform()); | ||
makeDraggable(this.rotateCircle, (_deltaPosition, offset) => { | ||
this.handleRotateCircleDrag(offset); | ||
}, () => this.finishDragging()); | ||
}, () => this.finalizeTransform()); | ||
} | ||
@@ -164,5 +165,3 @@ // Note a small change in the position of this' background while dragging | ||
deltaPosition = this.editor.viewport.roundPoint(deltaPosition); | ||
this.region = this.region.translatedBy(deltaPosition); | ||
this.transform = this.transform.rightMul(Mat33.translation(deltaPosition)); | ||
this.previewTransformCmds(); | ||
this.transformPreview(Mat33.translation(deltaPosition)); | ||
} | ||
@@ -176,14 +175,7 @@ handleResizeCornerDrag(deltaPosition) { | ||
if (newSize.y > 0 && newSize.x > 0) { | ||
this.region = this.region.resizedTo(newSize); | ||
const scaleFactor = Vec2.of(this.region.w / oldWidth, this.region.h / oldHeight); | ||
const currentTransfm = Mat33.scaling2D(scaleFactor, this.region.topLeft); | ||
this.transform = this.transform.rightMul(currentTransfm); | ||
this.previewTransformCmds(); | ||
const scaleFactor = Vec2.of(newSize.x / oldWidth, newSize.y / oldHeight); | ||
this.transformPreview(Mat33.scaling2D(scaleFactor, this.region.topLeft)); | ||
} | ||
} | ||
handleRotateCircleDrag(offset) { | ||
this.boxRotation = this.boxRotation % (2 * Math.PI); | ||
if (this.boxRotation < 0) { | ||
this.boxRotation += 2 * Math.PI; | ||
} | ||
let targetRotation = offset.angle(); | ||
@@ -205,5 +197,3 @@ targetRotation = targetRotation % (2 * Math.PI); | ||
} | ||
this.transform = this.transform.rightMul(Mat33.zRotation(deltaRotation, this.region.center)); | ||
this.boxRotation += deltaRotation; | ||
this.previewTransformCmds(); | ||
this.transformPreview(Mat33.zRotation(deltaRotation, this.region.center)); | ||
} | ||
@@ -215,4 +205,21 @@ computeTransformCommands() { | ||
} | ||
// Applies, previews, but doesn't finalize the given transformation. | ||
transformPreview(transform) { | ||
this.transform = this.transform.rightMul(transform); | ||
const deltaRotation = transform.transformVec3(Vec2.unitX).angle(); | ||
transform = transform.rightMul(Mat33.zRotation(-deltaRotation, this.region.center)); | ||
this.boxRotation += deltaRotation; | ||
this.boxRotation = this.boxRotation % (2 * Math.PI); | ||
if (this.boxRotation < 0) { | ||
this.boxRotation += 2 * Math.PI; | ||
} | ||
const newSize = transform.transformVec3(this.region.size); | ||
const translation = transform.transformVec2(this.region.topLeft).minus(this.region.topLeft); | ||
this.region = this.region.resizedTo(newSize); | ||
this.region = this.region.translatedBy(translation); | ||
this.previewTransformCmds(); | ||
this.scrollTo(); | ||
} | ||
// Applies the current transformation to the selection | ||
finishDragging() { | ||
finalizeTransform() { | ||
this.transformationCommands.forEach(cmd => { | ||
@@ -337,2 +344,12 @@ cmd.unapply(this.editor); | ||
} | ||
// Scroll the viewport to this. Does not zoom | ||
scrollTo() { | ||
const viewport = this.editor.viewport; | ||
const visibleRect = viewport.visibleRect; | ||
if (!visibleRect.containsPoint(this.region.center)) { | ||
const closestPoint = visibleRect.getClosestPointOnBoundaryTo(this.region.center); | ||
const delta = this.region.center.minus(closestPoint); | ||
this.editor.dispatchNoAnnounce(new Viewport.ViewportTransform(Mat33.translation(delta.times(-1))), false); | ||
} | ||
} | ||
deleteSelectedObjects() { | ||
@@ -394,2 +411,3 @@ return new Erase(this.selectedElems); | ||
}); | ||
this.editor.handleKeyEventsFrom(this.handleOverlay); | ||
} | ||
@@ -424,2 +442,7 @@ onPointerDown(event) { | ||
this.editor.announceForAccessibility(this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount())); | ||
this.zoomToSelection(); | ||
} | ||
} | ||
zoomToSelection() { | ||
if (this.selectionBox) { | ||
const selectionRect = this.selectionBox.region; | ||
@@ -442,2 +465,76 @@ this.editor.dispatchNoAnnounce(this.editor.viewport.zoomTo(selectionRect, false), false); | ||
} | ||
onKeyPress(event) { | ||
let rotationSteps = 0; | ||
let xTranslateSteps = 0; | ||
let yTranslateSteps = 0; | ||
let xScaleSteps = 0; | ||
let yScaleSteps = 0; | ||
switch (event.key) { | ||
case 'a': | ||
case 'h': | ||
case 'ArrowLeft': | ||
xTranslateSteps -= 1; | ||
break; | ||
case 'd': | ||
case 'l': | ||
case 'ArrowRight': | ||
xTranslateSteps += 1; | ||
break; | ||
case 'q': | ||
case 'k': | ||
case 'ArrowUp': | ||
yTranslateSteps -= 1; | ||
break; | ||
case 'e': | ||
case 'j': | ||
case 'ArrowDown': | ||
yTranslateSteps += 1; | ||
break; | ||
case 'r': | ||
rotationSteps += 1; | ||
break; | ||
case 'R': | ||
rotationSteps -= 1; | ||
break; | ||
case 'i': | ||
xScaleSteps -= 1; | ||
break; | ||
case 'I': | ||
xScaleSteps += 1; | ||
break; | ||
case 'o': | ||
yScaleSteps -= 1; | ||
break; | ||
case 'O': | ||
yScaleSteps += 1; | ||
break; | ||
} | ||
let handled = xTranslateSteps !== 0 | ||
|| yTranslateSteps !== 0 | ||
|| rotationSteps !== 0 | ||
|| xScaleSteps !== 0 | ||
|| yScaleSteps !== 0; | ||
if (!this.selectionBox) { | ||
handled = false; | ||
} | ||
else if (handled) { | ||
const translateStepSize = 10 * this.editor.viewport.getSizeOfPixelOnCanvas(); | ||
const rotateStepSize = Math.PI / 8; | ||
const scaleStepSize = translateStepSize / 2; | ||
const region = this.selectionBox.region; | ||
const scaledSize = this.selectionBox.region.size.plus(Vec2.of(xScaleSteps, yScaleSteps).times(scaleStepSize)); | ||
const transform = Mat33.scaling2D(Vec2.of( | ||
// Don't more-than-half the size of the selection | ||
Math.max(0.5, scaledSize.x / region.size.x), Math.max(0.5, scaledSize.y / region.size.y)), region.topLeft).rightMul(Mat33.zRotation(rotationSteps * rotateStepSize, region.center)).rightMul(Mat33.translation(Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize))); | ||
this.selectionBox.transformPreview(transform); | ||
} | ||
return handled; | ||
} | ||
onKeyUp(evt) { | ||
if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) { | ||
this.selectionBox.finalizeTransform(); | ||
return true; | ||
} | ||
return false; | ||
} | ||
setEnabled(enabled) { | ||
@@ -449,2 +546,9 @@ super.setEnabled(enabled); | ||
this.handleOverlay.style.display = enabled ? 'block' : 'none'; | ||
if (enabled) { | ||
this.handleOverlay.tabIndex = 0; | ||
this.handleOverlay.ariaLabel = this.editor.localization.selectionToolKeyboardShortcuts; | ||
} | ||
else { | ||
this.handleOverlay.tabIndex = -1; | ||
} | ||
} | ||
@@ -465,1 +569,9 @@ // Get the object responsible for displaying this' selection. | ||
} | ||
SelectionTool.handleableKeys = [ | ||
'a', 'h', 'ArrowLeft', | ||
'd', 'l', 'ArrowRight', | ||
'q', 'k', 'ArrowUp', | ||
'e', 'j', 'ArrowDown', | ||
'r', 'R', | ||
'i', 'I', 'o', 'O', | ||
]; |
@@ -26,2 +26,3 @@ import { InputEvtType, EditorEventType } from '../types'; | ||
const panZoomTool = new PanZoom(editor, PanZoomMode.TwoFingerTouchGestures | PanZoomMode.RightClickDrags, localization.touchPanTool); | ||
const keyboardPanZoomTool = new PanZoom(editor, PanZoomMode.Keyboard, localization.keyboardPanZoom); | ||
const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 16 }); | ||
@@ -42,2 +43,3 @@ const primaryTools = [ | ||
...primaryTools, | ||
keyboardPanZoomTool, | ||
new UndoRedoShortcut(editor), | ||
@@ -81,4 +83,5 @@ ]; | ||
} | ||
else if (event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent) { | ||
else if (event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent || event.kind === InputEvtType.KeyUpEvent) { | ||
const isKeyPressEvt = event.kind === InputEvtType.KeyPressEvent; | ||
const isKeyReleaseEvt = event.kind === InputEvtType.KeyUpEvent; | ||
const isWheelEvt = event.kind === InputEvtType.WheelEvt; | ||
@@ -91,3 +94,4 @@ for (const tool of this.tools) { | ||
const keyPressResult = isKeyPressEvt && tool.onKeyPress(event); | ||
handled = keyPressResult || wheelResult; | ||
const keyReleaseResult = isKeyReleaseEvt && tool.onKeyUp(event); | ||
handled = keyPressResult || wheelResult || keyReleaseResult; | ||
if (handled) { | ||
@@ -94,0 +98,0 @@ break; |
@@ -22,3 +22,4 @@ import EventDispatcher from './EventDispatcher'; | ||
WheelEvt = 4, | ||
KeyPressEvent = 5 | ||
KeyPressEvent = 5, | ||
KeyUpEvent = 6 | ||
} | ||
@@ -35,2 +36,7 @@ export interface WheelEvt { | ||
} | ||
export interface KeyUpEvent { | ||
readonly kind: InputEvtType.KeyUpEvent; | ||
readonly key: string; | ||
readonly ctrlKey: boolean; | ||
} | ||
export interface GestureCancelEvt { | ||
@@ -53,3 +59,3 @@ readonly kind: InputEvtType.GestureCancelEvt; | ||
export declare type PointerEvt = PointerDownEvt | PointerMoveEvt | PointerUpEvt; | ||
export declare type InputEvt = KeyPressEvent | WheelEvt | GestureCancelEvt | PointerEvt; | ||
export declare type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt; | ||
export declare type EditorNotifier = EventDispatcher<EditorEventType, EditorEventDataType>; | ||
@@ -56,0 +62,0 @@ export declare enum EditorEventType { |
@@ -10,2 +10,3 @@ // Types related to the image editor | ||
InputEvtType[InputEvtType["KeyPressEvent"] = 5] = "KeyPressEvent"; | ||
InputEvtType[InputEvtType["KeyUpEvent"] = 6] = "KeyUpEvent"; | ||
})(InputEvtType || (InputEvtType = {})); | ||
@@ -12,0 +13,0 @@ export var EditorEventType; |
@@ -23,2 +23,5 @@ import { EditorEventType } from './types'; | ||
this.undoStack.push(command); | ||
for (const elem of this.redoStack) { | ||
elem.onDrop(this.editor); | ||
} | ||
this.redoStack = []; | ||
@@ -25,0 +28,0 @@ this.fireUpdateEvent(); |
{ | ||
"name": "js-draw", | ||
"version": "0.1.9", | ||
"version": "0.1.10", | ||
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ", | ||
@@ -12,2 +12,10 @@ "main": "dist/src/Editor.js", | ||
}, | ||
"./localizations/getLocalizationTable": { | ||
"types": "./dist/src/localizations/getLocalizationTable.d.ts", | ||
"default": "./dist/src/localizations/getLocalizationTable.js" | ||
}, | ||
"./getLocalizationTable": { | ||
"types": "./dist/src/localizations/getLocalizationTable.d.ts", | ||
"default": "./dist/src/localizations/getLocalizationTable.js" | ||
}, | ||
"./styles": { | ||
@@ -14,0 +22,0 @@ "default": "./src/styles.js" |
@@ -121,7 +121,19 @@ # js-draw | ||
See [src/localization.ts](src/localization.ts) for a list of strings. | ||
If a user's language is available in [src/localizations/](src/localizations/) (as determined by `navigator.languages`), that localization will be used. | ||
Some of the default strings in the editor might be overridden like this: | ||
To override the default language, use `getLocalizationTable([ 'custom locale here' ])`. For example, | ||
```ts | ||
const editor = new Editor(document.body, { | ||
// Force the Spanish (Español) localizaiton | ||
localization: getLocalizationTable([ 'es' ]), | ||
}); | ||
``` | ||
<details><summary>Creating a custom localization</summary> | ||
See [src/localization.ts](src/localization.ts) for a list of strings that can be translated. | ||
Many of the default strings in the editor might be overridden like this: | ||
```ts | ||
const editor = new Editor(document.body, { | ||
// Example partial Spanish localization | ||
@@ -145,3 +157,2 @@ localization: { | ||
select: 'Selecciona', | ||
touchDrawing: 'Dibuja con un dedo', | ||
thicknessLabel: 'Tamaño: ', | ||
@@ -155,2 +166,3 @@ colorLabel: 'Color: ', | ||
</details> | ||
@@ -157,0 +169,0 @@ ## Changing the editor's color theme |
@@ -5,4 +5,5 @@ // Main entrypoint for Webpack when building a bundle for release. | ||
import Editor from '../Editor'; | ||
import getLocalizationTable from '../localizations/getLocalizationTable'; | ||
export default Editor; | ||
export { Editor }; | ||
export { Editor, getLocalizationTable }; |
@@ -20,3 +20,4 @@ | ||
import Rect2 from './geometry/Rect2'; | ||
import { defaultEditorLocalization, EditorLocalization } from './localization'; | ||
import { EditorLocalization } from './localization'; | ||
import getLocalizationTable from './localizations/getLocalizationTable'; | ||
@@ -47,3 +48,3 @@ export interface EditorSettings { | ||
private importExportViewport: Viewport; | ||
public localization: EditorLocalization = defaultEditorLocalization; | ||
public localization: EditorLocalization; | ||
@@ -64,3 +65,3 @@ public viewport: Viewport; | ||
this.localization = { | ||
...this.localization, | ||
...getLocalizationTable(), | ||
...settings.localization, | ||
@@ -244,13 +245,3 @@ }; | ||
this.renderingRegion.addEventListener('keydown', evt => { | ||
if (this.toolController.dispatchInputEvent({ | ||
kind: InputEvtType.KeyPressEvent, | ||
key: evt.key, | ||
ctrlKey: evt.ctrlKey, | ||
})) { | ||
evt.preventDefault(); | ||
} else if (evt.key === 'Escape') { | ||
this.renderingRegion.blur(); | ||
} | ||
}); | ||
this.handleKeyEventsFrom(this.renderingRegion); | ||
@@ -314,2 +305,28 @@ this.container.addEventListener('wheel', evt => { | ||
// Adds event listners for keypresses to [elem] and forwards those events to the | ||
// editor. | ||
public handleKeyEventsFrom(elem: HTMLElement) { | ||
elem.addEventListener('keydown', evt => { | ||
if (this.toolController.dispatchInputEvent({ | ||
kind: InputEvtType.KeyPressEvent, | ||
key: evt.key, | ||
ctrlKey: evt.ctrlKey, | ||
})) { | ||
evt.preventDefault(); | ||
} else if (evt.key === 'Escape') { | ||
this.renderingRegion.blur(); | ||
} | ||
}); | ||
elem.addEventListener('keyup', evt => { | ||
if (this.toolController.dispatchInputEvent({ | ||
kind: InputEvtType.KeyUpEvent, | ||
key: evt.key, | ||
ctrlKey: evt.ctrlKey, | ||
})) { | ||
evt.preventDefault(); | ||
} | ||
}); | ||
} | ||
// Adds to history by default | ||
@@ -316,0 +333,0 @@ public dispatch(command: Command, addToHistory: boolean = true) { |
@@ -77,2 +77,17 @@ import LineSegment2 from './LineSegment2'; | ||
}); | ||
it('Closest point to (0,0) on the line x = 1 should be (1,0)', () => { | ||
const line = new LineSegment2(Vec2.of(1, 100), Vec2.of(1, -100)); | ||
expect(line.closestPointTo(Vec2.zero)).objEq(Vec2.of(1, 0)); | ||
}); | ||
it('Closest point from (-1,2) to segment((1,1) -> (2,4)) should be (1,1)', () => { | ||
const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4)); | ||
expect(line.closestPointTo(Vec2.of(-1, 2))).objEq(Vec2.of(1, 1)); | ||
}); | ||
it('Closest point from (5,2) to segment((1,1) -> (2,4)) should be (2,4)', () => { | ||
const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4)); | ||
expect(line.closestPointTo(Vec2.of(5, 2))).objEq(Vec2.of(2, 4)); | ||
}); | ||
}); |
@@ -10,2 +10,3 @@ import Rect2 from './Rect2'; | ||
export default class LineSegment2 { | ||
// invariant: ||direction|| = 1 | ||
public readonly direction: Vec2; | ||
@@ -128,2 +129,21 @@ public readonly length: number; | ||
} | ||
// Returns the closest point on this to [target] | ||
public closestPointTo(target: Point2) { | ||
// Distance from P1 along this' direction. | ||
const projectedDistFromP1 = target.minus(this.p1).dot(this.direction); | ||
const projectedDistFromP2 = this.length - projectedDistFromP1; | ||
const projection = this.p1.plus(this.direction.times(projectedDistFromP1)); | ||
if (projectedDistFromP1 > 0 && projectedDistFromP1 < this.length) { | ||
return projection; | ||
} | ||
if (Math.abs(projectedDistFromP2) < Math.abs(projectedDistFromP1)) { | ||
return this.p2; | ||
} else { | ||
return this.p1; | ||
} | ||
} | ||
} |
@@ -150,12 +150,25 @@ | ||
}); | ||
it('division of rectangle', () => { | ||
expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject( | ||
[ | ||
new Rect2(0, 0, 1, 0.5), new Rect2(1, 0, 1, 0.5), | ||
new Rect2(0, 0.5, 1, 0.5), new Rect2(1, 0.5, 1, 0.5), | ||
] | ||
); | ||
}); | ||
}); | ||
it('division of rectangle', () => { | ||
expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject( | ||
[ | ||
new Rect2(0, 0, 1, 0.5), new Rect2(1, 0, 1, 0.5), | ||
new Rect2(0, 0.5, 1, 0.5), new Rect2(1, 0.5, 1, 0.5), | ||
] | ||
); | ||
describe('should correctly return the closest point on the edge of a rectangle', () => { | ||
it('with the unit square', () => { | ||
const rect = Rect2.unitSquare; | ||
expect(rect.getClosestPointOnBoundaryTo(Vec2.zero)).objEq(Vec2.zero); | ||
expect(rect.getClosestPointOnBoundaryTo(Vec2.of(-1, -1))).objEq(Vec2.zero); | ||
expect(rect.getClosestPointOnBoundaryTo(Vec2.of(-1, 0.5))).objEq(Vec2.of(0, 0.5)); | ||
expect(rect.getClosestPointOnBoundaryTo(Vec2.of(1, 0.5))).objEq(Vec2.of(1, 0.5)); | ||
expect(rect.getClosestPointOnBoundaryTo(Vec2.of(0.6, 0.6))).objEq(Vec2.of(1, 0.6)); | ||
expect(rect.getClosestPointOnBoundaryTo(Vec2.of(2, 0.5))).objEq(Vec2.of(1, 0.5)); | ||
expect(rect.getClosestPointOnBoundaryTo(Vec2.of(0.6, 0.6))).objEq(Vec2.of(1, 0.6)); | ||
}); | ||
}); | ||
}); |
@@ -54,2 +54,3 @@ import LineSegment2 from './LineSegment2'; | ||
// Returns a copy of this with the given size (but same top-left). | ||
public resizedTo(size: Vec2): Rect2 { | ||
@@ -167,2 +168,19 @@ return new Rect2(this.x, this.y, size.x, size.y); | ||
public getClosestPointOnBoundaryTo(target: Point2) { | ||
const closestEdgePoints = this.getEdges().map(edge => { | ||
return edge.closestPointTo(target); | ||
}); | ||
let closest: Point2|null = null; | ||
let closestDist: number|null = null; | ||
for (const point of closestEdgePoints) { | ||
const dist = point.minus(target).length(); | ||
if (closestDist === null || dist < closestDist) { | ||
closest = point; | ||
closestDist = dist; | ||
} | ||
} | ||
return closest!; | ||
} | ||
public get corners(): Point2[] { | ||
@@ -177,3 +195,3 @@ return [ | ||
public get maxDimension(): number { | ||
public get maxDimension() { | ||
return Math.max(this.w, this.h); | ||
@@ -180,0 +198,0 @@ } |
@@ -141,3 +141,3 @@ import Editor from '../Editor'; | ||
const undoButton = this.addActionButton({ | ||
label: 'Undo', | ||
label: this.localizationTable.undo, | ||
icon: makeUndoIcon() | ||
@@ -148,3 +148,3 @@ }, () => { | ||
const redoButton = this.addActionButton({ | ||
label: 'Redo', | ||
label: this.localizationTable.redo, | ||
icon: makeRedoIcon(), | ||
@@ -205,8 +205,5 @@ }, () => { | ||
for (const tool of toolController.getMatchingTools(ToolType.PanZoom)) { | ||
if (!(tool instanceof PanZoom)) { | ||
throw new Error('All SelectionTools must have kind === ToolType.PanZoom'); | ||
} | ||
(new HandToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container); | ||
const panZoomTool = toolController.getMatchingTools(ToolType.PanZoom)[0]; | ||
if (panZoomTool && panZoomTool instanceof PanZoom) { | ||
(new HandToolWidget(this.editor, panZoomTool, this.localizationTable)).addTo(this.container); | ||
} | ||
@@ -213,0 +210,0 @@ |
@@ -12,7 +12,7 @@ import Color4 from '../Color4'; | ||
const svgNamespace = 'http://www.w3.org/2000/svg'; | ||
const primaryForegroundFill = ` | ||
style='fill: var(--primary-foreground-color);' | ||
const iconColorFill = ` | ||
style='fill: var(--icon-color);' | ||
`; | ||
const primaryForegroundStrokeFill = ` | ||
style='fill: var(--primary-foreground-color); stroke: var(--primary-foreground-color);' | ||
const iconColorStrokeFill = ` | ||
style='fill: var(--icon-color); stroke: var(--icon-color);' | ||
`; | ||
@@ -43,3 +43,3 @@ const checkerboardPatternDef = ` | ||
.toolbar-svg-undo-redo-icon { | ||
stroke: var(--primary-foreground-color); | ||
stroke: var(--icon-color); | ||
stroke-width: 12; | ||
@@ -68,3 +68,3 @@ stroke-linejoin: round; | ||
d='M5,10 L50,90 L95,10 Z' | ||
${primaryForegroundFill} | ||
${iconColorFill} | ||
/> | ||
@@ -86,3 +86,3 @@ </g> | ||
x=10 y=10 width=80 height=50 | ||
${primaryForegroundFill} | ||
${iconColorFill} | ||
/> | ||
@@ -139,3 +139,3 @@ </g> | ||
style=' | ||
stroke: var(--primary-foreground-color); | ||
stroke: var(--icon-color); | ||
stroke-width: 2; | ||
@@ -183,3 +183,3 @@ ' | ||
style=' | ||
stroke: var(--primary-foreground-color); | ||
stroke: var(--icon-color); | ||
stroke-width: 2; | ||
@@ -250,3 +250,3 @@ ' | ||
style=' | ||
stroke: var(--primary-foreground-color); | ||
stroke: var(--icon-color); | ||
stroke-width: 2; | ||
@@ -273,3 +273,3 @@ ' | ||
textNode.style.fontSize = '55px'; | ||
textNode.style.fill = 'var(--primary-foreground-color)'; | ||
textNode.style.fill = 'var(--icon-color)'; | ||
textNode.style.fontFamily = 'monospace'; | ||
@@ -326,3 +326,3 @@ | ||
d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z' | ||
${primaryForegroundStrokeFill} | ||
${iconColorStrokeFill} | ||
/> | ||
@@ -401,3 +401,3 @@ </g> | ||
`); | ||
pipette.style.fill = 'var(--primary-foreground-color)'; | ||
pipette.style.fill = 'var(--icon-color)'; | ||
@@ -404,0 +404,0 @@ if (color) { |
@@ -26,2 +26,3 @@ | ||
zoom: string; | ||
selectionToolKeyboardShortcuts: string; | ||
@@ -50,2 +51,3 @@ dropdownShown: (toolName: string)=> string; | ||
pickColorFronScreen: 'Pick color from screen', | ||
selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.', | ||
@@ -52,0 +54,0 @@ touchPanning: 'Touchscreen panning', |
import Editor from '../../Editor'; | ||
import { InputEvtType } from '../../types'; | ||
import { toolbarCSSPrefix } from '../HTMLToolbar'; | ||
@@ -53,2 +54,35 @@ import { makeDropdownIcon } from '../icons'; | ||
protected setupActionBtnClickListener(button: HTMLElement) { | ||
const clickTriggers = { enter: true, ' ': true, }; | ||
button.onkeydown = (evt) => { | ||
let handled = false; | ||
if (evt.key in clickTriggers) { | ||
if (!this.disabled) { | ||
this.handleClick(); | ||
handled = true; | ||
} | ||
} | ||
// If we didn't do anything with the event, send it to the editor. | ||
if (!handled) { | ||
this.editor.toolController.dispatchInputEvent({ | ||
kind: InputEvtType.KeyPressEvent, | ||
key: evt.key, | ||
ctrlKey: evt.ctrlKey, | ||
}); | ||
} | ||
}; | ||
button.onkeyup = evt => { | ||
if (evt.key in clickTriggers) { | ||
return; | ||
} | ||
this.editor.toolController.dispatchInputEvent({ | ||
kind: InputEvtType.KeyUpEvent, | ||
key: evt.key, | ||
ctrlKey: evt.ctrlKey, | ||
}); | ||
}; | ||
button.onclick = () => { | ||
@@ -55,0 +89,0 @@ if (!this.disabled) { |
@@ -1,2 +0,2 @@ | ||
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent } from '../types'; | ||
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent, KeyUpEvent } from '../types'; | ||
import { ToolType } from './ToolController'; | ||
@@ -27,2 +27,6 @@ import ToolEnabledGroup from './ToolEnabledGroup'; | ||
public onKeyUp(_event: KeyUpEvent): boolean { | ||
return false; | ||
} | ||
public setEnabled(enabled: boolean) { | ||
@@ -29,0 +33,0 @@ this.enabled = enabled; |
export interface ToolLocalization { | ||
keyboardPanZoom: string; | ||
penTool: (penId: number)=>string; | ||
@@ -28,2 +29,3 @@ selectionTool: string; | ||
pipetteTool: 'Pick color from screen', | ||
keyboardPanZoom: 'Keyboard pan/zoom shortcuts', | ||
@@ -30,0 +32,0 @@ textTool: 'Text', |
@@ -19,2 +19,3 @@ | ||
export enum PanZoomMode { | ||
@@ -25,2 +26,3 @@ OneFingerTouchGestures = 0x1, | ||
SinglePointerGestures = 0x1 << 3, | ||
Keyboard = 0x1 << 4, | ||
} | ||
@@ -185,2 +187,6 @@ | ||
public onKeyPress({ key }: KeyPressEvent): boolean { | ||
if (!(this.mode & PanZoomMode.Keyboard)) { | ||
return false; | ||
} | ||
let translation = Vec2.zero; | ||
@@ -187,0 +193,0 @@ let scale = 1; |
@@ -69,3 +69,3 @@ /* @jest-environment jsdom */ | ||
selection!.handleBackgroundDrag(Vec2.of(5, 5)); | ||
selection!.finishDragging(); | ||
selection!.finalizeTransform(); | ||
@@ -84,2 +84,25 @@ expect(testStroke.getBBox().topLeft).toMatchObject({ | ||
}); | ||
it('moving the selection with a keyboard should move the view to keep the selection in view', () => { | ||
const { addTestStrokeCommand } = createSquareStroke(); | ||
const editor = createEditor(); | ||
editor.dispatch(addTestStrokeCommand); | ||
// Select the stroke | ||
const selectionTool = getSelectionTool(editor); | ||
selectionTool.setEnabled(true); | ||
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0)); | ||
editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10, 10)); | ||
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(100, 100)); | ||
const selection = selectionTool.getSelection(); | ||
if (selection === null) { | ||
// Throw to allow TypeScript's non-null checker to understand that selection | ||
// must be non-null after this. | ||
throw new Error('Selection should be non-null.'); | ||
} | ||
selection.handleBackgroundDrag(Vec2.of(0, -1000)); | ||
expect(editor.viewport.visibleRect.containsPoint(selection.region.center)).toBe(true); | ||
}); | ||
}); |
@@ -11,3 +11,4 @@ import Command from '../commands/Command'; | ||
import { EditorLocalization } from '../localization'; | ||
import { EditorEventType, PointerEvt } from '../types'; | ||
import { EditorEventType, KeyPressEvent, KeyUpEvent, PointerEvt } from '../types'; | ||
import Viewport from '../Viewport'; | ||
import BaseTool from './BaseTool'; | ||
@@ -167,11 +168,11 @@ import { ToolType } from './ToolController'; | ||
this.handleBackgroundDrag(deltaPosition); | ||
}, () => this.finishDragging()); | ||
}, () => this.finalizeTransform()); | ||
makeDraggable(resizeCorner, (deltaPosition) => { | ||
this.handleResizeCornerDrag(deltaPosition); | ||
}, () => this.finishDragging()); | ||
}, () => this.finalizeTransform()); | ||
makeDraggable(this.rotateCircle, (_deltaPosition, offset) => { | ||
this.handleRotateCircleDrag(offset); | ||
}, () => this.finishDragging()); | ||
}, () => this.finalizeTransform()); | ||
} | ||
@@ -191,6 +192,3 @@ | ||
this.region = this.region.translatedBy(deltaPosition); | ||
this.transform = this.transform.rightMul(Mat33.translation(deltaPosition)); | ||
this.previewTransformCmds(); | ||
this.transformPreview(Mat33.translation(deltaPosition)); | ||
} | ||
@@ -209,8 +207,5 @@ | ||
if (newSize.y > 0 && newSize.x > 0) { | ||
this.region = this.region.resizedTo(newSize); | ||
const scaleFactor = Vec2.of(this.region.w / oldWidth, this.region.h / oldHeight); | ||
const scaleFactor = Vec2.of(newSize.x / oldWidth, newSize.y / oldHeight); | ||
const currentTransfm = Mat33.scaling2D(scaleFactor, this.region.topLeft); | ||
this.transform = this.transform.rightMul(currentTransfm); | ||
this.previewTransformCmds(); | ||
this.transformPreview(Mat33.scaling2D(scaleFactor, this.region.topLeft)); | ||
} | ||
@@ -220,7 +215,2 @@ } | ||
public handleRotateCircleDrag(offset: Vec2) { | ||
this.boxRotation = this.boxRotation % (2 * Math.PI); | ||
if (this.boxRotation < 0) { | ||
this.boxRotation += 2 * Math.PI; | ||
} | ||
let targetRotation = offset.angle(); | ||
@@ -245,5 +235,3 @@ targetRotation = targetRotation % (2 * Math.PI); | ||
this.transform = this.transform.rightMul(Mat33.zRotation(deltaRotation, this.region.center)); | ||
this.boxRotation += deltaRotation; | ||
this.previewTransformCmds(); | ||
this.transformPreview(Mat33.zRotation(deltaRotation, this.region.center)); | ||
} | ||
@@ -257,4 +245,25 @@ | ||
// Applies, previews, but doesn't finalize the given transformation. | ||
public transformPreview(transform: Mat33) { | ||
this.transform = this.transform.rightMul(transform); | ||
const deltaRotation = transform.transformVec3(Vec2.unitX).angle(); | ||
transform = transform.rightMul(Mat33.zRotation(-deltaRotation, this.region.center)); | ||
this.boxRotation += deltaRotation; | ||
this.boxRotation = this.boxRotation % (2 * Math.PI); | ||
if (this.boxRotation < 0) { | ||
this.boxRotation += 2 * Math.PI; | ||
} | ||
const newSize = transform.transformVec3(this.region.size); | ||
const translation = transform.transformVec2(this.region.topLeft).minus(this.region.topLeft); | ||
this.region = this.region.resizedTo(newSize); | ||
this.region = this.region.translatedBy(translation); | ||
this.previewTransformCmds(); | ||
this.scrollTo(); | ||
} | ||
// Applies the current transformation to the selection | ||
public finishDragging() { | ||
public finalizeTransform() { | ||
this.transformationCommands.forEach(cmd => { | ||
@@ -331,3 +340,2 @@ cmd.unapply(this.editor); | ||
public appendBackgroundBoxTo(elem: HTMLElement) { | ||
@@ -455,2 +463,15 @@ if (this.backgroundBox.parentElement) { | ||
// Scroll the viewport to this. Does not zoom | ||
public scrollTo() { | ||
const viewport = this.editor.viewport; | ||
const visibleRect = viewport.visibleRect; | ||
if (!visibleRect.containsPoint(this.region.center)) { | ||
const closestPoint = visibleRect.getClosestPointOnBoundaryTo(this.region.center); | ||
const delta = this.region.center.minus(closestPoint); | ||
this.editor.dispatchNoAnnounce( | ||
new Viewport.ViewportTransform(Mat33.translation(delta.times(-1))), false | ||
); | ||
} | ||
} | ||
public deleteSelectedObjects(): Command { | ||
@@ -485,2 +506,4 @@ return new Erase(this.selectedElems); | ||
}); | ||
this.editor.handleKeyEventsFrom(this.handleOverlay); | ||
} | ||
@@ -525,3 +548,8 @@ | ||
); | ||
this.zoomToSelection(); | ||
} | ||
} | ||
private zoomToSelection() { | ||
if (this.selectionBox) { | ||
const selectionRect = this.selectionBox.region; | ||
@@ -546,2 +574,102 @@ this.editor.dispatchNoAnnounce(this.editor.viewport.zoomTo(selectionRect, false), false); | ||
private static handleableKeys = [ | ||
'a', 'h', 'ArrowLeft', | ||
'd', 'l', 'ArrowRight', | ||
'q', 'k', 'ArrowUp', | ||
'e', 'j', 'ArrowDown', | ||
'r', 'R', | ||
'i', 'I', 'o', 'O', | ||
]; | ||
public onKeyPress(event: KeyPressEvent): boolean { | ||
let rotationSteps = 0; | ||
let xTranslateSteps = 0; | ||
let yTranslateSteps = 0; | ||
let xScaleSteps = 0; | ||
let yScaleSteps = 0; | ||
switch (event.key) { | ||
case 'a': | ||
case 'h': | ||
case 'ArrowLeft': | ||
xTranslateSteps -= 1; | ||
break; | ||
case 'd': | ||
case 'l': | ||
case 'ArrowRight': | ||
xTranslateSteps += 1; | ||
break; | ||
case 'q': | ||
case 'k': | ||
case 'ArrowUp': | ||
yTranslateSteps -= 1; | ||
break; | ||
case 'e': | ||
case 'j': | ||
case 'ArrowDown': | ||
yTranslateSteps += 1; | ||
break; | ||
case 'r': | ||
rotationSteps += 1; | ||
break; | ||
case 'R': | ||
rotationSteps -= 1; | ||
break; | ||
case 'i': | ||
xScaleSteps -= 1; | ||
break; | ||
case 'I': | ||
xScaleSteps += 1; | ||
break; | ||
case 'o': | ||
yScaleSteps -= 1; | ||
break; | ||
case 'O': | ||
yScaleSteps += 1; | ||
break; | ||
} | ||
let handled = xTranslateSteps !== 0 | ||
|| yTranslateSteps !== 0 | ||
|| rotationSteps !== 0 | ||
|| xScaleSteps !== 0 | ||
|| yScaleSteps !== 0; | ||
if (!this.selectionBox) { | ||
handled = false; | ||
} else if (handled) { | ||
const translateStepSize = 10 * this.editor.viewport.getSizeOfPixelOnCanvas(); | ||
const rotateStepSize = Math.PI / 8; | ||
const scaleStepSize = translateStepSize / 2; | ||
const region = this.selectionBox.region; | ||
const scaledSize = this.selectionBox.region.size.plus( | ||
Vec2.of(xScaleSteps, yScaleSteps).times(scaleStepSize) | ||
); | ||
const transform = Mat33.scaling2D( | ||
Vec2.of( | ||
// Don't more-than-half the size of the selection | ||
Math.max(0.5, scaledSize.x / region.size.x), | ||
Math.max(0.5, scaledSize.y / region.size.y) | ||
), | ||
region.topLeft | ||
).rightMul(Mat33.zRotation( | ||
rotationSteps * rotateStepSize, region.center | ||
)).rightMul(Mat33.translation( | ||
Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize) | ||
)); | ||
this.selectionBox.transformPreview(transform); | ||
} | ||
return handled; | ||
} | ||
public onKeyUp(evt: KeyUpEvent) { | ||
if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) { | ||
this.selectionBox.finalizeTransform(); | ||
return true; | ||
} | ||
return false; | ||
} | ||
public setEnabled(enabled: boolean) { | ||
@@ -555,2 +683,9 @@ super.setEnabled(enabled); | ||
this.handleOverlay.style.display = enabled ? 'block' : 'none'; | ||
if (enabled) { | ||
this.handleOverlay.tabIndex = 0; | ||
this.handleOverlay.ariaLabel = this.editor.localization.selectionToolKeyboardShortcuts; | ||
} else { | ||
this.handleOverlay.tabIndex = -1; | ||
} | ||
} | ||
@@ -557,0 +692,0 @@ |
@@ -33,2 +33,3 @@ import { InputEvtType, InputEvt, EditorEventType } from '../types'; | ||
const panZoomTool = new PanZoom(editor, PanZoomMode.TwoFingerTouchGestures | PanZoomMode.RightClickDrags, localization.touchPanTool); | ||
const keyboardPanZoomTool = new PanZoom(editor, PanZoomMode.Keyboard, localization.keyboardPanZoom); | ||
const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 16 }); | ||
@@ -52,2 +53,3 @@ const primaryTools = [ | ||
...primaryTools, | ||
keyboardPanZoomTool, | ||
new UndoRedoShortcut(editor), | ||
@@ -93,5 +95,6 @@ ]; | ||
} else if ( | ||
event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent | ||
event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent || event.kind === InputEvtType.KeyUpEvent | ||
) { | ||
const isKeyPressEvt = event.kind === InputEvtType.KeyPressEvent; | ||
const isKeyReleaseEvt = event.kind === InputEvtType.KeyUpEvent; | ||
const isWheelEvt = event.kind === InputEvtType.WheelEvt; | ||
@@ -105,3 +108,4 @@ for (const tool of this.tools) { | ||
const keyPressResult = isKeyPressEvt && tool.onKeyPress(event); | ||
handled = keyPressResult || wheelResult; | ||
const keyReleaseResult = isKeyReleaseEvt && tool.onKeyUp(event); | ||
handled = keyPressResult || wheelResult || keyReleaseResult; | ||
@@ -108,0 +112,0 @@ if (handled) { |
@@ -27,2 +27,3 @@ // Types related to the image editor | ||
export enum InputEvtType { | ||
@@ -36,2 +37,3 @@ PointerDownEvt, | ||
KeyPressEvent, | ||
KeyUpEvent | ||
} | ||
@@ -54,2 +56,8 @@ | ||
export interface KeyUpEvent { | ||
readonly kind: InputEvtType.KeyUpEvent; | ||
readonly key: string; | ||
readonly ctrlKey: boolean; | ||
} | ||
// Event triggered when pointer capture is taken by a different [PointerEvtListener]. | ||
@@ -78,3 +86,3 @@ export interface GestureCancelEvt { | ||
export type PointerEvt = PointerDownEvt | PointerMoveEvt | PointerUpEvt; | ||
export type InputEvt = KeyPressEvent | WheelEvt | GestureCancelEvt | PointerEvt; | ||
export type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt; | ||
@@ -81,0 +89,0 @@ export type EditorNotifier = EventDispatcher<EditorEventType, EditorEventDataType>; |
@@ -35,2 +35,6 @@ import Editor from './Editor'; | ||
this.undoStack.push(command); | ||
for (const elem of this.redoStack) { | ||
elem.onDrop(this.editor); | ||
} | ||
this.redoStack = []; | ||
@@ -37,0 +41,0 @@ this.fireUpdateEvent(); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
970462
2678
270
20548
206
19
119
0
3
0