2017-10-26 20:28:17 -04:00
|
|
|
import paper from '@scratch/paper';
|
2020-02-06 18:11:10 -05:00
|
|
|
import {CROSSHAIR_SIZE, getBackgroundGuideLayer, getDragCrosshairLayer, getRaster} from './layer';
|
2020-04-21 18:04:26 -04:00
|
|
|
import {getAllRootItems, getSelectedRootItems} from './selection';
|
2018-08-31 12:07:17 -04:00
|
|
|
import {getHitBounds} from './bitmap';
|
2020-04-21 18:04:26 -04:00
|
|
|
import log from '../log/log';
|
2017-10-26 20:28:17 -04:00
|
|
|
|
2018-05-01 16:18:24 -04:00
|
|
|
// Vectors are imported and exported at SVG_ART_BOARD size.
|
|
|
|
// Once they are imported however, both SVGs and bitmaps are on
|
|
|
|
// canvases of ART_BOARD size.
|
2020-04-21 18:04:26 -04:00
|
|
|
// (This is for backwards compatibility, to handle both assets
|
|
|
|
// designed for 480 x 360, and bitmap resolution 2 bitmaps)
|
2018-05-01 16:18:24 -04:00
|
|
|
const SVG_ART_BOARD_WIDTH = 480;
|
|
|
|
const SVG_ART_BOARD_HEIGHT = 360;
|
2020-04-21 18:04:26 -04:00
|
|
|
const ART_BOARD_WIDTH = SVG_ART_BOARD_WIDTH * 2;
|
|
|
|
const ART_BOARD_HEIGHT = SVG_ART_BOARD_HEIGHT * 2;
|
2020-02-08 00:49:48 -05:00
|
|
|
const CENTER = new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2);
|
2018-08-31 12:07:17 -04:00
|
|
|
const PADDING_PERCENT = 25; // Padding as a percent of the max of width/height of the sprite
|
2020-04-21 18:04:26 -04:00
|
|
|
const BUFFER = 50; // Number of pixels of allowance around objects at the edges of the workspace
|
2018-08-31 12:07:17 -04:00
|
|
|
const MIN_RATIO = .125; // Zoom in to at least 1/8 of the screen. This way you don't end up incredibly
|
2020-04-21 18:04:26 -04:00
|
|
|
// zoomed in for tiny costumes.
|
2020-05-11 18:42:52 -04:00
|
|
|
const OUTERMOST_ZOOM_LEVEL = 0.333;
|
2020-04-21 18:04:26 -04:00
|
|
|
const ART_BOARD_BOUNDS = new paper.Rectangle(0, 0, ART_BOARD_WIDTH, ART_BOARD_HEIGHT);
|
|
|
|
const MAX_WORKSPACE_BOUNDS = new paper.Rectangle(
|
|
|
|
-ART_BOARD_WIDTH / 4,
|
|
|
|
-ART_BOARD_HEIGHT / 4,
|
|
|
|
ART_BOARD_WIDTH * 1.5,
|
|
|
|
ART_BOARD_HEIGHT * 1.5);
|
|
|
|
|
|
|
|
let _workspaceBounds = ART_BOARD_BOUNDS;
|
|
|
|
|
|
|
|
const getWorkspaceBounds = () => _workspaceBounds;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The workspace bounds define the areas that the scroll bars can access.
|
|
|
|
* They include at minimum the artboard, and extend to a bit beyond the
|
|
|
|
* farthest item off tne edge in any given direction (so items can't be
|
|
|
|
* "lost" off the edge)
|
|
|
|
*
|
|
|
|
* @param {boolean} clipEmpty Clip empty space from bounds, even if it
|
|
|
|
* means discontinuously jumping the viewport. This should probably be
|
|
|
|
* false unless the viewport is going to move discontinuously anyway
|
|
|
|
* (such as in a zoom button click)
|
|
|
|
*/
|
|
|
|
const setWorkspaceBounds = clipEmpty => {
|
|
|
|
const items = getAllRootItems();
|
|
|
|
// Include the artboard and what's visible in the viewport
|
|
|
|
let bounds = ART_BOARD_BOUNDS;
|
|
|
|
if (!clipEmpty) {
|
|
|
|
bounds = bounds.unite(paper.view.bounds);
|
|
|
|
}
|
|
|
|
// Include everything the user has drawn and a buffer around it
|
|
|
|
for (const item of items) {
|
|
|
|
bounds = bounds.unite(item.bounds.expand(BUFFER));
|
|
|
|
}
|
|
|
|
// Limit to max workspace bounds
|
|
|
|
bounds = bounds.intersect(MAX_WORKSPACE_BOUNDS.expand(BUFFER));
|
|
|
|
let top = bounds.top;
|
|
|
|
let left = bounds.left;
|
|
|
|
let bottom = bounds.bottom;
|
|
|
|
let right = bounds.right;
|
|
|
|
|
|
|
|
// Center in view if viewport is larger than workspace
|
|
|
|
let hDiff = 0;
|
|
|
|
let vDiff = 0;
|
|
|
|
if (bounds.width < paper.view.bounds.width) {
|
|
|
|
hDiff = (paper.view.bounds.width - bounds.width) / 2;
|
|
|
|
left -= hDiff;
|
|
|
|
right += hDiff;
|
|
|
|
}
|
|
|
|
if (bounds.height < paper.view.bounds.height) {
|
|
|
|
vDiff = (paper.view.bounds.height - bounds.height) / 2;
|
|
|
|
top -= vDiff;
|
|
|
|
bottom += vDiff;
|
|
|
|
}
|
|
|
|
|
|
|
|
_workspaceBounds = new paper.Rectangle(left, top, right - left, bottom - top);
|
|
|
|
};
|
2018-04-16 18:08:17 -04:00
|
|
|
|
2018-08-16 16:49:43 -04:00
|
|
|
const clampViewBounds = () => {
|
2017-10-27 09:33:06 -04:00
|
|
|
const {left, right, top, bottom} = paper.project.view.bounds;
|
2020-04-21 18:04:26 -04:00
|
|
|
if (left < _workspaceBounds.left) {
|
|
|
|
paper.project.view.scrollBy(new paper.Point(_workspaceBounds.left - left, 0));
|
2017-10-27 09:33:06 -04:00
|
|
|
}
|
2020-04-21 18:04:26 -04:00
|
|
|
if (top < _workspaceBounds.top) {
|
|
|
|
paper.project.view.scrollBy(new paper.Point(0, _workspaceBounds.top - top));
|
2017-10-27 09:33:06 -04:00
|
|
|
}
|
2020-04-21 18:04:26 -04:00
|
|
|
if (bottom > _workspaceBounds.bottom) {
|
|
|
|
paper.project.view.scrollBy(new paper.Point(0, _workspaceBounds.bottom - bottom));
|
2017-10-27 09:33:06 -04:00
|
|
|
}
|
2020-04-21 18:04:26 -04:00
|
|
|
if (right > _workspaceBounds.right) {
|
|
|
|
paper.project.view.scrollBy(new paper.Point(_workspaceBounds.right - right, 0));
|
2017-10-27 09:33:06 -04:00
|
|
|
}
|
2020-04-21 18:04:26 -04:00
|
|
|
setWorkspaceBounds();
|
2017-10-27 09:33:06 -04:00
|
|
|
};
|
|
|
|
|
2020-02-13 12:00:23 -05:00
|
|
|
const resizeCrosshair = () => {
|
2020-02-06 18:11:10 -05:00
|
|
|
if (getDragCrosshairLayer() && getDragCrosshairLayer().dragCrosshair) {
|
|
|
|
getDragCrosshairLayer().dragCrosshair.scale(
|
|
|
|
CROSSHAIR_SIZE / getDragCrosshairLayer().dragCrosshair.bounds.width / paper.view.zoom);
|
|
|
|
}
|
|
|
|
if (getBackgroundGuideLayer() && getBackgroundGuideLayer().dragCrosshair) {
|
|
|
|
getBackgroundGuideLayer().dragCrosshair.scale(
|
|
|
|
CROSSHAIR_SIZE / getBackgroundGuideLayer().dragCrosshair.bounds.width / paper.view.zoom);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-10-27 10:06:01 -04:00
|
|
|
// Zoom keeping a project-space point fixed.
|
|
|
|
// This article was helpful http://matthiasberth.com/tech/stable-zoom-and-pan-in-paperjs
|
|
|
|
const zoomOnFixedPoint = (deltaZoom, fixedPoint) => {
|
2018-08-31 12:07:17 -04:00
|
|
|
const view = paper.view;
|
2017-10-27 10:06:01 -04:00
|
|
|
const preZoomCenter = view.center;
|
2020-05-11 18:42:52 -04:00
|
|
|
const newZoom = Math.max(OUTERMOST_ZOOM_LEVEL, view.zoom + deltaZoom);
|
2017-10-27 10:06:01 -04:00
|
|
|
const scaling = view.zoom / newZoom;
|
|
|
|
const preZoomOffset = fixedPoint.subtract(preZoomCenter);
|
|
|
|
const postZoomOffset = fixedPoint.subtract(preZoomOffset.multiply(scaling))
|
|
|
|
.subtract(preZoomCenter);
|
|
|
|
view.zoom = newZoom;
|
|
|
|
view.translate(postZoomOffset.multiply(-1));
|
2020-04-21 18:04:26 -04:00
|
|
|
|
|
|
|
setWorkspaceBounds(true /* clipEmpty */);
|
2018-08-16 16:49:43 -04:00
|
|
|
clampViewBounds();
|
2020-02-13 12:00:23 -05:00
|
|
|
resizeCrosshair();
|
2017-10-27 10:06:01 -04:00
|
|
|
};
|
|
|
|
|
2017-10-26 20:28:17 -04:00
|
|
|
// Zoom keeping the selection center (if any) fixed.
|
2017-10-27 09:33:06 -04:00
|
|
|
const zoomOnSelection = deltaZoom => {
|
2017-10-26 20:28:17 -04:00
|
|
|
let fixedPoint;
|
|
|
|
const items = getSelectedRootItems();
|
|
|
|
if (items.length > 0) {
|
|
|
|
let rect = null;
|
|
|
|
for (const item of items) {
|
|
|
|
if (rect) {
|
|
|
|
rect = rect.unite(item.bounds);
|
|
|
|
} else {
|
|
|
|
rect = item.bounds;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fixedPoint = rect.center;
|
|
|
|
} else {
|
|
|
|
fixedPoint = paper.project.view.center;
|
|
|
|
}
|
|
|
|
zoomOnFixedPoint(deltaZoom, fixedPoint);
|
|
|
|
};
|
|
|
|
|
|
|
|
const resetZoom = () => {
|
2018-04-16 18:08:17 -04:00
|
|
|
paper.project.view.zoom = .5;
|
2020-04-21 18:04:26 -04:00
|
|
|
setWorkspaceBounds(true /* clipEmpty */);
|
2020-02-13 12:00:23 -05:00
|
|
|
resizeCrosshair();
|
2018-08-16 16:49:43 -04:00
|
|
|
clampViewBounds();
|
2017-10-26 20:28:17 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
const pan = (dx, dy) => {
|
|
|
|
paper.project.view.scrollBy(new paper.Point(dx, dy));
|
2018-08-16 16:49:43 -04:00
|
|
|
clampViewBounds();
|
2017-10-26 20:28:17 -04:00
|
|
|
};
|
|
|
|
|
2020-04-21 18:04:26 -04:00
|
|
|
/**
|
|
|
|
* Mouse actions are clamped to action bounds
|
|
|
|
* @param {boolean} isBitmap True if the editor is in bitmap mode, false if it is in vector mode
|
|
|
|
* @returns {paper.Rectangle} the bounds within which mouse events should work in the paint editor
|
|
|
|
*/
|
|
|
|
const getActionBounds = isBitmap => {
|
|
|
|
if (isBitmap) {
|
|
|
|
return ART_BOARD_BOUNDS;
|
|
|
|
}
|
|
|
|
return paper.view.bounds.unite(ART_BOARD_BOUNDS).intersect(MAX_WORKSPACE_BOUNDS);
|
|
|
|
};
|
|
|
|
|
2018-08-31 12:07:17 -04:00
|
|
|
const zoomToFit = isBitmap => {
|
|
|
|
resetZoom();
|
|
|
|
let bounds;
|
|
|
|
if (isBitmap) {
|
2020-04-21 18:04:26 -04:00
|
|
|
bounds = getHitBounds(getRaster()).expand(BUFFER);
|
2018-08-31 12:07:17 -04:00
|
|
|
} else {
|
2020-04-21 18:04:26 -04:00
|
|
|
const items = getAllRootItems();
|
|
|
|
for (const item of items) {
|
|
|
|
if (bounds) {
|
|
|
|
bounds = bounds.unite(item.bounds);
|
|
|
|
} else {
|
|
|
|
bounds = item.bounds;
|
|
|
|
}
|
|
|
|
}
|
2018-08-31 12:07:17 -04:00
|
|
|
}
|
|
|
|
if (bounds && bounds.width && bounds.height) {
|
2020-04-21 18:04:26 -04:00
|
|
|
const canvas = paper.view.element;
|
|
|
|
// Ratio of (sprite length plus padding on all sides) to viewport length.
|
|
|
|
let ratio = paper.view.zoom *
|
|
|
|
Math.max(
|
|
|
|
bounds.width * (1 + (2 * PADDING_PERCENT / 100)) / canvas.clientWidth,
|
|
|
|
bounds.height * (1 + (2 * PADDING_PERCENT / 100)) / canvas.clientHeight);
|
2018-08-31 12:07:17 -04:00
|
|
|
// Clamp ratio
|
|
|
|
ratio = Math.max(Math.min(1, ratio), MIN_RATIO);
|
|
|
|
if (ratio < 1) {
|
|
|
|
paper.view.center = bounds.center;
|
|
|
|
paper.view.zoom = paper.view.zoom / ratio;
|
2020-02-13 12:00:23 -05:00
|
|
|
resizeCrosshair();
|
2018-08-31 12:07:17 -04:00
|
|
|
clampViewBounds();
|
|
|
|
}
|
2020-04-21 18:04:26 -04:00
|
|
|
} else {
|
|
|
|
log.warn('No bounds!');
|
2018-08-31 12:07:17 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-10-26 20:28:17 -04:00
|
|
|
export {
|
2020-04-21 18:04:26 -04:00
|
|
|
ART_BOARD_BOUNDS,
|
2018-04-16 18:08:17 -04:00
|
|
|
ART_BOARD_HEIGHT,
|
|
|
|
ART_BOARD_WIDTH,
|
2020-02-08 00:49:48 -05:00
|
|
|
CENTER,
|
2020-05-11 18:42:52 -04:00
|
|
|
OUTERMOST_ZOOM_LEVEL,
|
2018-05-01 16:18:24 -04:00
|
|
|
SVG_ART_BOARD_WIDTH,
|
|
|
|
SVG_ART_BOARD_HEIGHT,
|
2020-04-21 18:04:26 -04:00
|
|
|
MAX_WORKSPACE_BOUNDS,
|
2018-08-16 16:49:43 -04:00
|
|
|
clampViewBounds,
|
2020-04-21 18:04:26 -04:00
|
|
|
getActionBounds,
|
2017-10-26 20:28:17 -04:00
|
|
|
pan,
|
|
|
|
resetZoom,
|
2020-04-21 18:04:26 -04:00
|
|
|
setWorkspaceBounds,
|
|
|
|
getWorkspaceBounds,
|
2020-02-13 12:00:23 -05:00
|
|
|
resizeCrosshair,
|
2017-10-26 20:28:17 -04:00
|
|
|
zoomOnSelection,
|
2018-08-31 12:07:17 -04:00
|
|
|
zoomOnFixedPoint,
|
|
|
|
zoomToFit
|
2017-10-27 09:33:06 -04:00
|
|
|
};
|