Improve new scrollbars on scene editor canvas (#4411)

Do not show in changelog
This commit is contained in:
AlexandreS
2022-10-19 11:22:07 +02:00
committed by GitHub
parent 80cb6d697c
commit d5eecda570
6 changed files with 246 additions and 204 deletions

View File

@@ -9,12 +9,12 @@ import { useScreenType } from '../UI/Reponsive/ScreenTypeMeasurer';
import { FullSizeMeasurer } from '../UI/FullSizeMeasurer';
import useForceUpdate from '../Utils/UseForceUpdate';
import { useDebounce } from '../Utils/UseDebounce';
import Rectangle from '../Utils/Rectangle';
const SCROLLBAR_DETECTION_WIDTH = 50;
const SCROLLBAR_TRACK_WIDTH = 16;
// Those scrollbar dimensions should be the same as in the CSS file Scrollbar.css
const SCROLLBAR_THUMB_WIDTH = 8;
const SCROLLBAR_SIZE = 200;
const SCROLLBAR_MARGIN = (SCROLLBAR_TRACK_WIDTH - SCROLLBAR_THUMB_WIDTH) / 2;
const THROTTLE_TIME = 1000 / 60; // 60 FPS
@@ -22,56 +22,6 @@ const styles = {
container: {
overflow: 'hidden',
},
xScrollbarDetectionZone: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
height: SCROLLBAR_DETECTION_WIDTH,
},
xScrollbarTrack: {
position: 'relative',
display: 'inline-block',
width: '100%',
marginTop: SCROLLBAR_DETECTION_WIDTH - SCROLLBAR_TRACK_WIDTH,
marginRight: SCROLLBAR_TRACK_WIDTH - SCROLLBAR_MARGIN, // leave some margin for the vertical scrollbar
height: SCROLLBAR_TRACK_WIDTH,
},
xThumb: {
position: 'relative',
width: SCROLLBAR_SIZE,
marginTop: SCROLLBAR_MARGIN,
height: SCROLLBAR_THUMB_WIDTH,
backgroundColor: '#1D1D26',
outline: '2px solid #FAFAFA',
opacity: 0.3,
borderRadius: 4,
},
yScrollbarDetectionZone: {
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
width: SCROLLBAR_DETECTION_WIDTH,
},
yScrollbarTrack: {
position: 'relative',
display: 'inline-block',
height: '100%',
marginLeft: SCROLLBAR_DETECTION_WIDTH - SCROLLBAR_TRACK_WIDTH,
marginBottom: SCROLLBAR_TRACK_WIDTH - SCROLLBAR_MARGIN, // leave some margin for the vertical scrollbar
width: SCROLLBAR_TRACK_WIDTH,
},
yThumb: {
position: 'relative',
height: SCROLLBAR_SIZE,
marginLeft: SCROLLBAR_MARGIN,
width: SCROLLBAR_THUMB_WIDTH,
backgroundColor: '#1D1D26',
outline: '1px solid #FAFAFA',
opacity: 0.3,
borderRadius: 4,
},
};
type Props = {|
@@ -85,28 +35,38 @@ const FullSizeInstancesEditorWithScrollbars = (props: Props) => {
const { wrappedEditorRef, ...otherProps } = props;
const editorRef = React.useRef<?InstancesEditor>(null);
const xScrollbarDetectionZone = React.useRef<?HTMLDivElement>(null);
const xScrollbarTrack = React.useRef<?HTMLDivElement>(null);
const xScrollbarThumb = React.useRef<?HTMLDivElement>(null);
const yScrollbarDetectionZone = React.useRef<?HTMLDivElement>(null);
const yScrollbarTrack = React.useRef<?HTMLDivElement>(null);
const yScrollbarThumb = React.useRef<?HTMLDivElement>(null);
const showScrollbars = React.useRef(false);
const showScrollbars = React.useRef<boolean>(false);
const timeoutHidingScrollbarsId = React.useRef<?TimeoutID>(null);
const isDragging = React.useRef(false);
const isDragging = React.useRef<boolean>(false);
const xValue = React.useRef(0);
const yValue = React.useRef(0);
const xMin = React.useRef(-5000);
const xMax = React.useRef(5000);
const yMin = React.useRef(-5000);
const yMax = React.useRef(5000);
const canvasWidth = React.useRef(0);
const canvasHeight = React.useRef(0);
const xValue = React.useRef<number>(0);
const yValue = React.useRef<number>(0);
const xMin = React.useRef<number>(-5000);
const xMax = React.useRef<number>(5000);
const yMin = React.useRef<number>(-5000);
const yMax = React.useRef<number>(5000);
const canvasWidth = React.useRef<number>(0);
const canvasHeight = React.useRef<number>(0);
const forceUpdate = useForceUpdate();
const canvasRectangle = React.useMemo(() => new Rectangle(), []);
if (editorRef.current) {
const elementBoundingRect = editorRef.current.getBoundingClientRect();
canvasRectangle.set({
left: elementBoundingRect.left,
top: elementBoundingRect.top,
right: elementBoundingRect.right - SCROLLBAR_DETECTION_WIDTH,
bottom: elementBoundingRect.bottom - SCROLLBAR_DETECTION_WIDTH,
});
}
const hideScrollbarsAfterDelay = React.useCallback(
() => {
if (timeoutHidingScrollbarsId.current) {
@@ -124,14 +84,54 @@ const FullSizeInstancesEditorWithScrollbars = (props: Props) => {
hideScrollbarsAfterDelay();
}, 500);
// If the mouse gets out of the detection zone, whilst not dragging, hide the scrollbars.
const mouseOutDetectionZoneHandler = React.useCallback(
(e: MouseEvent) => {
if (!isDragging.current) {
hideScrollbarsAfterDelay();
const showScrollbarsThrottled = throttle(
() => {
if (!showScrollbars.current) {
showScrollbars.current = true;
forceUpdate();
}
if (timeoutHidingScrollbarsId.current) {
clearTimeout(timeoutHidingScrollbarsId.current);
timeoutHidingScrollbarsId.current = null;
}
},
[hideScrollbarsAfterDelay]
1000,
{ leading: true, trailing: false }
);
const hideScrollbarsAfterDelayThrottled = throttle(
hideScrollbarsAfterDelay,
1000,
{ leading: true, trailing: false }
);
const onMouseMoveOverInstanceEditor = React.useCallback(
(event: MouseEvent) => {
if (!editorRef.current) {
return;
}
const shouldDisplayScrollBars = !canvasRectangle.containsPoint(
event.clientX,
event.clientY
);
if (shouldDisplayScrollBars) {
if (timeoutHidingScrollbarsId.current) {
clearTimeout(timeoutHidingScrollbarsId.current);
}
if (!showScrollbars.current) showScrollbarsThrottled();
} else {
if (!isDragging.current && showScrollbars.current) {
hideScrollbarsAfterDelayThrottled();
}
}
},
[
canvasRectangle,
hideScrollbarsAfterDelayThrottled,
showScrollbarsThrottled,
]
);
// When the mouse is moving after dragging the thumb:
@@ -192,14 +192,11 @@ const FullSizeInstancesEditorWithScrollbars = (props: Props) => {
);
// When the user releases the thumb, we need to stop listening to mouse move and up events.
// In the case the user releases outside of the detection zone, we need to hide the scrollbars.
const makeMouseUpXThumbHandler = React.useCallback(
mouseMoveHandler =>
function mouseUpHandler(e: MouseEvent) {
isDragging.current = false;
// If the user releases the mouse outside of the detection zone, we want to hide the scrollbars.
if (
e.target !== xScrollbarDetectionZone.current &&
e.target !== xScrollbarTrack.current &&
e.target !== xScrollbarThumb.current
) {
@@ -214,9 +211,7 @@ const FullSizeInstancesEditorWithScrollbars = (props: Props) => {
mouseMoveHandler =>
function mouseUpHandler(e: MouseEvent) {
isDragging.current = false;
// If the user releases the mouse outside of the detection zone, we want to hide the scrollbars.
if (
e.target !== yScrollbarDetectionZone.current &&
e.target !== yScrollbarTrack.current &&
e.target !== yScrollbarThumb.current
) {
@@ -284,50 +279,6 @@ const FullSizeInstancesEditorWithScrollbars = (props: Props) => {
[mouseDownXThumbHandler, mouseDownYThumbHandler]
);
// When the mouse is over the detection zone, show the scrollbars,
// and ensure the timeout to hide them is cleared.
const mouseOverDetectionZoneHandler = React.useCallback(
(e: MouseEvent) => {
if (!showScrollbars.current) {
showScrollbars.current = true;
forceUpdate();
}
if (timeoutHidingScrollbarsId.current) {
clearTimeout(timeoutHidingScrollbarsId.current);
timeoutHidingScrollbarsId.current = null;
}
},
[forceUpdate]
);
// Add the mouse over and out events once on mount.
React.useEffect(
() => {
const xScrollbarDetectionZoneElement = xScrollbarDetectionZone.current;
const yScrollbarDetectionZoneElement = yScrollbarDetectionZone.current;
if (!xScrollbarDetectionZoneElement || !yScrollbarDetectionZoneElement)
return;
xScrollbarDetectionZoneElement.addEventListener(
'mouseover',
mouseOverDetectionZoneHandler
);
xScrollbarDetectionZoneElement.addEventListener(
'mouseout',
mouseOutDetectionZoneHandler
);
yScrollbarDetectionZoneElement.addEventListener(
'mouseover',
mouseOverDetectionZoneHandler
);
yScrollbarDetectionZoneElement.addEventListener(
'mouseout',
mouseOutDetectionZoneHandler
);
},
[mouseOverDetectionZoneHandler, mouseOutDetectionZoneHandler]
);
const setAndAdjust = React.useCallback(
({
newXValue,
@@ -376,9 +327,18 @@ const FullSizeInstancesEditorWithScrollbars = (props: Props) => {
[setAndAdjust]
);
const onMouseEnterThumb = (event: MouseEvent) => {
if (timeoutHidingScrollbarsId.current) {
clearTimeout(timeoutHidingScrollbarsId.current);
}
};
const onMouseLeaveThumb = (event: MouseEvent) => {
if (!isDragging.current) hideScrollbarsAfterDelay();
};
// Ensure the X Scrollbar doesn't go out of bounds.
const minXScrollbarLeftPosition = '0%';
const maxXScrollbarLeftPosition = `calc(100% - ${SCROLLBAR_SIZE}px - ${SCROLLBAR_TRACK_WIDTH}px)`;
const maxXScrollbarLeftPosition = `calc(100% - ${SCROLLBAR_SIZE}px - ${SCROLLBAR_THUMB_WIDTH}px)`;
const expectedXScrollbarLeftPosition = `calc(${((xValue.current -
xMin.current) /
(xMax.current - xMin.current)) *
@@ -388,7 +348,7 @@ const FullSizeInstancesEditorWithScrollbars = (props: Props) => {
// Ensure the Y Scrollbar doesn't go out of bounds.
const minYScrollbarTopPosition = '0%';
const maxYScrollbarTopPosition = `calc(100% - ${SCROLLBAR_SIZE}px - ${SCROLLBAR_TRACK_WIDTH}px)`;
const maxYScrollbarTopPosition = `calc(100% - ${SCROLLBAR_SIZE}px - ${SCROLLBAR_THUMB_WIDTH}px)`;
const expectedYScrollbarTopPosition = `calc(${((yValue.current -
yMin.current) /
(yMax.current - yMin.current)) *
@@ -414,53 +374,65 @@ const FullSizeInstancesEditorWithScrollbars = (props: Props) => {
width={width}
height={height}
screenType={screenType}
onMouseMove={onMouseMoveOverInstanceEditor}
onMouseLeave={(event: MouseEvent) => {
const { relatedTarget } = event;
if (!isDragging.current && relatedTarget) {
if (
// Flow says className is not present in ElementTarget but this piece
// of code cannot break.
// $FlowFixMe
relatedTarget.className &&
typeof relatedTarget.className === 'string' &&
// Hide only if the mouse is not leaving to go on one of the scrollbars' thumb.
// $FlowFixMe
!relatedTarget.className.includes('canvas-scrollbar-thumb')
) {
hideScrollbarsAfterDelay();
}
}
}}
{...otherProps}
/>
)}
{screenType !== 'touch' && (
<div
style={styles.yScrollbarDetectionZone}
ref={yScrollbarDetectionZone}
style={{
// Keep it in the DOM, so we can register the mouse down event.
visibility: showScrollbars.current ? 'visible' : 'hidden',
}}
className="canvas-vertical-scrollbar-track"
ref={yScrollbarTrack}
>
<div
style={{
...styles.yScrollbarTrack,
// Keep it in the DOM, so we can register the mouse down event.
visibility: showScrollbars.current ? 'visible' : 'hidden',
top: yScrollbarTopPosition,
}}
ref={yScrollbarTrack}
>
<div
style={{
...styles.yThumb,
top: yScrollbarTopPosition,
}}
ref={yScrollbarThumb}
/>
</div>
className="canvas-scrollbar-thumb canvas-vertical-scrollbar-thumb"
ref={yScrollbarThumb}
onMouseEnter={onMouseEnterThumb}
onMouseLeave={onMouseLeaveThumb}
/>
</div>
)}
{screenType !== 'touch' && (
<div
style={styles.xScrollbarDetectionZone}
ref={xScrollbarDetectionZone}
style={{
// Keep it in the DOM, so we can register the mouse down event.
visibility: showScrollbars.current ? 'visible' : 'hidden',
}}
className="canvas-horizontal-scrollbar-track"
ref={xScrollbarTrack}
>
<div
style={{
...styles.xScrollbarTrack,
// Keep it in the DOM, so we can register the mouse down event.
visibility: showScrollbars.current ? 'visible' : 'hidden',
marginLeft: xScrollbarLeftPosition,
}}
ref={xScrollbarTrack}
>
<div
style={{
...styles.xThumb,
marginLeft: xScrollbarLeftPosition,
}}
ref={xScrollbarThumb}
/>
</div>
className="canvas-scrollbar-thumb canvas-horizontal-scrollbar-thumb"
ref={xScrollbarThumb}
onMouseEnter={onMouseEnterThumb}
onMouseLeave={onMouseLeaveThumb}
/>
</div>
)}
</div>

View File

@@ -85,6 +85,8 @@ type Props = {|
width: number,
height: number,
onViewPositionChanged: ViewPosition => void,
onMouseMove: MouseEvent => void,
onMouseLeave: MouseEvent => void,
screenType: ScreenType,
|};
@@ -179,16 +181,13 @@ export default class InstancesEditor extends Component<Props> {
this.zoomOnCursorBy(-event.deltaY / 5000);
} else if (this.keyboardShortcuts.shouldScrollHorizontally()) {
const deltaX = event.deltaY / (5 * zoomFactor);
this.viewPosition.scrollBy(-deltaX, 0);
this.scrollBy(-deltaX, 0);
} else {
const deltaX = event.deltaX / (5 * zoomFactor);
const deltaY = event.deltaY / (5 * zoomFactor);
this.viewPosition.scrollBy(deltaX, deltaY);
this.scrollBy(deltaX, deltaY);
}
if (this.props.onViewPositionChanged) {
this.props.onViewPositionChanged(this.viewPosition);
}
event.preventDefault();
};
this.pixiRenderer.view.setAttribute('tabIndex', -1);
@@ -208,6 +207,12 @@ export default class InstancesEditor extends Component<Props> {
'mouseup',
this.keyboardShortcuts.onMouseUp
);
this.pixiRenderer.view.addEventListener('mousemove', event =>
this.props.onMouseMove(event)
);
this.pixiRenderer.view.addEventListener('mouseout', event => {
this.props.onMouseLeave(event);
});
this.pixiContainer = new PIXI.Container();
@@ -518,13 +523,10 @@ export default class InstancesEditor extends Component<Props> {
this.setZoomFactor(this.getZoomFactor() + value);
const afterZoomCursorPosition = this.getLastCursorSceneCoordinates();
// Compensate for the cursor change in position
this.viewPosition.scrollBy(
this.scrollBy(
beforeZoomCursorPosition[0] - afterZoomCursorPosition[0],
beforeZoomCursorPosition[1] - afterZoomCursorPosition[1]
);
if (this.props.onViewPositionChanged) {
this.props.onViewPositionChanged(this.viewPosition);
}
}
getZoomFactor = () => {
@@ -582,11 +584,7 @@ export default class InstancesEditor extends Component<Props> {
const sceneDeltaX = deltaX / this.getZoomFactor();
const sceneDeltaY = deltaY / this.getZoomFactor();
this.viewPosition.scrollBy(-sceneDeltaX, -sceneDeltaY);
if (this.props.onViewPositionChanged) {
this.props.onViewPositionChanged(this.viewPosition);
}
this.scrollBy(-sceneDeltaX, -sceneDeltaY);
} else {
this.selectionRectangle.updateSelectionRectangle(x, y);
}
@@ -716,11 +714,7 @@ export default class InstancesEditor extends Component<Props> {
// to move the view, move it, then unpress it and continue to move the instance.
// This means that while we're in "_onMoveInstance", we must handle view moving.
if (this.keyboardShortcuts.shouldMoveView()) {
this.viewPosition.scrollBy(-sceneDeltaX, -sceneDeltaY);
if (this.props.onViewPositionChanged) {
this.props.onViewPositionChanged(this.viewPosition);
}
this.scrollBy(-sceneDeltaX, -sceneDeltaY);
return;
}
@@ -812,8 +806,35 @@ export default class InstancesEditor extends Component<Props> {
this.props.onInstancesMoved(unlockedSelectedInstances);
};
scrollBy(x: number, y: number) {
this.viewPosition.scrollBy(x, y);
if (this.props.onViewPositionChanged) {
this.props.onViewPositionChanged(this.viewPosition);
}
}
scrollTo(x: number, y: number) {
this.viewPosition.scrollTo(x, y);
if (this.props.onViewPositionChanged) {
this.props.onViewPositionChanged(this.viewPosition);
}
}
fitViewToRectangle(
rectangle: Rectangle,
{ adaptZoom }: { adaptZoom: boolean }
) {
const idealZoom = this.viewPosition.fitToRectangle(rectangle);
if (adaptZoom) this.setZoomFactor(idealZoom);
if (this.props.onViewPositionChanged) {
this.props.onViewPositionChanged(this.viewPosition);
}
}
getBoundingClientRect() {
if (!this.canvasArea) return { left: 0, top: 0, right: 0, bottom: 0 };
return this.canvasArea.getBoundingClientRect();
}
zoomToFitContent() {
@@ -844,17 +865,14 @@ export default class InstancesEditor extends Component<Props> {
// $FlowFixMe - JSFunctor is incompatible with Functor
initialInstances.iterateOverInstances(getInstanceRectangle);
getInstanceRectangle.delete();
if (contentAABB) {
const idealZoom = this.viewPosition.fitToRectangle(contentAABB);
this.setZoomFactor(idealZoom);
}
if (contentAABB) this.fitViewToRectangle(contentAABB, { adaptZoom: true });
}
zoomToInitialPosition() {
const x = this.props.project.getGameResolutionWidth() / 2;
const y = this.props.project.getGameResolutionHeight() / 2;
this.viewPosition.scrollTo(x, y);
this.setZoomFactor(1);
this.scrollTo(x, y);
}
zoomToFitSelection(instances: Array<gdInitialInstance>) {
@@ -870,10 +888,7 @@ export default class InstancesEditor extends Component<Props> {
instanceMeasurer.getInstanceAABB(instance, new Rectangle())
);
});
const idealZoom = this.viewPosition.fitToRectangle(
selectedInstancesRectangle
);
this.setZoomFactor(idealZoom);
this.fitViewToRectangle(selectedInstancesRectangle, { adaptZoom: true });
}
centerViewOnLastInstance(instances: Array<gdInitialInstance>) {
@@ -884,10 +899,7 @@ export default class InstancesEditor extends Component<Props> {
instances[instances.length - 1],
new Rectangle()
);
this.viewPosition.fitToRectangle(lastInstanceRectangle);
if (this.props.onViewPositionChanged) {
this.props.onViewPositionChanged(this.viewPosition);
}
this.fitViewToRectangle(lastInstanceRectangle, { adaptZoom: false });
}
getLastContextMenuSceneCoordinates = () => {

View File

@@ -23,7 +23,7 @@
/* Make the horizontal split to have the same thickness as the vertical */
.mosaic-gd-theme .mosaic-split.-column {
margin-top: -2px;
margin-top: -3px;
height: 4px;
}

View File

@@ -43,9 +43,10 @@
.mosaic-gd-theme .mosaic-split.-column:hover .mosaic-split-line {
background-color: var(--mosaic-toolbar-border-color-hover);
transform: scaleY(2);
transform: scaleY(2.5);
box-shadow: none !important;
cursor: ns-resize;
z-index: 5;
}
/* Separator draggable background (vertical split) */
@@ -60,7 +61,8 @@
.mosaic-gd-theme .mosaic-split.-row:hover .mosaic-split-line {
background-color: var(--mosaic-toolbar-border-color-hover);
transform: scaleX(2);
transform: scaleX(2.5);
box-shadow: none !important;
cursor: ew-resize;
z-index: 5;
}

View File

@@ -1,54 +1,106 @@
/* The default scrollbar for the app */
::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track,
::-webkit-scrollbar-thumb {
border: 1px solid transparent;
background-clip: padding-box;
border-radius: 4px;
border: 1px solid transparent;
background-clip: padding-box;
border-radius: 4px;
}
::-webkit-scrollbar-track {
background-color: rgba(0, 0, 0, 0.07);
background-color: rgba(0, 0, 0, 0.07);
}
::-webkit-scrollbar-thumb {
background-color: #8d8d8dcc;
background-color: #8d8d8dcc;
}
::-webkit-scrollbar-thumb:hover {
background-color: #8d8d8d;
background-color: #8d8d8d;
}
/** The scrollbar for tabs or toolbars: either invisible or very thing */
.almost-invisible-scrollbar {
/** For browsers not supporting -webkit-scrollbar, hide it because even the `thin` option is too large. */
scrollbar-width: none;
/** For browsers not supporting -webkit-scrollbar, hide it because even the `thin` option is too large. */
scrollbar-width: none;
}
.almost-invisible-scrollbar::-webkit-scrollbar {
width: 4px;
height: 4px;
width: 4px;
height: 4px;
}
.almost-invisible-scrollbar::-webkit-scrollbar-track,
.almost-invisible-scrollbar::-webkit-scrollbar-thumb {
border: 1px solid transparent;
background-clip: padding-box;
border-radius: 4px;
border: 1px solid transparent;
background-clip: padding-box;
border-radius: 4px;
}
.almost-invisible-scrollbar::-webkit-scrollbar-track {
background-color: rgba(0, 0, 0, 0.07);
background-color: rgba(0, 0, 0, 0.07);
}
.almost-invisible-scrollbar::-webkit-scrollbar-thumb {
background-color: #8d8d8dcc;
background-color: #8d8d8dcc;
}
.almost-invisible-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: #8d8d8d;
background-color: #8d8d8d;
}
/* Manually handled scrollbar for canvas */
body {
--canvas-scrollbar-border-radius: 4px;
--canvas-scrollbar-width: 8px;
--canvas-scrollbar-length: 200px;
--canvas-horizontal-scrollbar-padding: 3px;
--canvas-vertical-scrollbar-padding: 1px;
}
.canvas-scrollbar-thumb {
position: relative;
background-color: #8d8d8dcc;
border: 1px solid transparent;
box-sizing: border-box;
background-clip: content-box;
border-radius: var(--canvas-scrollbar-border-radius);
pointer-events: all;
}
.canvas-scrollbar-thumb:hover,
.canvas-scrollbar-thumb:active {
background-color: #8d8d8d;
}
.canvas-horizontal-scrollbar-track {
position: absolute;
left: 0px;
right: calc(
var(--canvas-scrollbar-width) - var(--canvas-vertical-scrollbar-padding)
);
bottom: 0px;
padding-bottom: var(--canvas-horizontal-scrollbar-padding);
pointer-events: none;
}
.canvas-horizontal-scrollbar-thumb {
width: var(--canvas-scrollbar-length);
height: var(--canvas-scrollbar-width);
}
.canvas-vertical-scrollbar-track {
position: absolute;
top: 0px;
bottom: calc(
var(--canvas-scrollbar-width) - var(--canvas-horizontal-scrollbar-padding)
);
right: 0px;
padding-right: var(--canvas-vertical-scrollbar-padding);
pointer-events: none;
}
.canvas-vertical-scrollbar-thumb {
height: var(--canvas-scrollbar-length);
width: var(--canvas-scrollbar-width);
}

View File

@@ -91,6 +91,10 @@ export default class Rectangle {
}
}
containsPoint(x: number, y: number): boolean {
return this.left <= x && this.right > x && this.bottom > y && this.top <= y;
}
toString() {
return (
'[' +