UNPKG

onsight

Version:

Interactive, easy to use JavaScript game framework.

658 lines (601 loc) 33.6 kB
import { OUTLINE_THICKNESS, } from '../../constants.js'; import { Box } from '../../core/objects/Box.js'; import { Box2 } from '../../math/Box2.js'; import { Circle } from '../../core/objects/Circle.js'; import { ColorStyle } from '../../core/objects/style/ColorStyle.js'; import { Line } from '../../core/objects/Line.js'; import { LinearGradientStyle } from '../../core/objects/style/LinearGradientStyle.js'; import { MathUtils } from '../../utils/MathUtils.js'; import { Matrix2 } from '../../math/Matrix2.js'; import { Object2D } from '../../core/Object2D.js'; import { Vector2 } from '../../math/Vector2.js'; // svg to base64, 'cursors/rotate.svg' const CURSOR_ROTATE = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjI7Ij48cGF0aCBkPSJNMjEuMjQ3LDUuODY3YzAuNDE3LC0wLjQ1MiAxLjAzNiwtMC42NjYgMS42NDcsLTAuNTYzYzAuNjQ0LDAuMTA5IDEuMTgsMC41NTMgMS40MDcsMS4xNjRsMS44MjQsNC45MDFjMC4yMjcsMC42MTEgMC4xMTEsMS4yOTggLTAuMzA1LDEuODAxYy0wLjQxNiwwLjUwMyAtMS4wNjksMC43NDUgLTEuNzEzLDAuNjM2bC01LjE1NCwtMC44NzRjLTAuNjQ0LC0wLjEwOSAtMS4xOCwtMC41NTMgLTEuNDA3LC0xLjE2NWMtMC4xNzksLTAuNDgxIC0wLjE0NSwtMS4wMDggMC4wOCwtMS40NTVjLTAuNTIxLC0wLjE0OCAtMS4wNjQsLTAuMjI1IC0xLjYxNSwtMC4yMjVjLTMuMjY0LDAgLTUuOTEzLDIuNjUgLTUuOTEzLDUuOTEzYy0wLDMuMjYzIDIuNjQ5LDUuOTEzIDUuOTEzLDUuOTEzYzEuNjQsMCAzLjIwNiwtMC42ODEgNC4zMjQsLTEuODhjMC42ODgsLTAuNzM4IDEuODQ0LC0wLjc3OCAyLjU4MiwtMC4wOWwxLjM0NiwxLjI1NWMwLjczNywwLjY4OCAwLjc3OCwxLjg0MyAwLjA5LDIuNTgxYy0yLjE1OCwyLjMxNCAtNS4xNzksMy42MjcgLTguMzQyLDMuNjI3Yy02LjI5NSwwIC0xMS40MDYsLTUuMTExIC0xMS40MDYsLTExLjQwNmMtMCwtNi4yOTUgNS4xMTEsLTExLjQwNiAxMS40MDYsLTExLjQwNmMxLjgzOCwtMCAzLjYzMSwwLjQ0MyA1LjIzNiwxLjI3M1oiIHN0eWxlPSJmaWxsOiNmZmY7Ii8+PHBhdGggZD0iTTE5LjgzNSw5Ljc2N2wtMC45MDUsMS4wOTNjLTAuMDk3LDAuMTE3IC0wLjEyNCwwLjI3NyAtMC4wNzEsMC40MTljMC4wNTMsMC4xNDMgMC4xNzgsMC4yNDYgMC4zMjgsMC4yNzJsNS4xNTQsMC44NzRjMC4xNTEsMC4wMjYgMC4zMDMsLTAuMDMxIDAuNCwtMC4xNDhjMC4wOTcsLTAuMTE3IDAuMTI0LC0wLjI3NyAwLjA3MSwtMC40MmwtMS44MjMsLTQuOWMtMC4wNTMsLTAuMTQzIC0wLjE3OCwtMC4yNDYgLTAuMzI4LC0wLjI3MWMtMC4xNSwtMC4wMjYgLTAuMzAyLDAuMDMxIC0wLjM5OSwwLjE0OGwtMC42OTksMC44NDRjLTEuNjMyLC0xLjA5MSAtMy41NjIsLTEuNjgzIC01LjU1MiwtMS42ODNjLTUuNTIyLC0wIC0xMC4wMDYsNC40ODMgLTEwLjAwNiwxMC4wMDVjMCw1LjUyMiA0LjQ4NCwxMC4wMDUgMTAuMDA2LDEwLjAwNWMyLjc3NSwwIDUuNDI1LC0xLjE1MiA3LjMxNywtMy4xODFjMC4xNjEsLTAuMTcyIDAuMTUxLC0wLjQ0MiAtMC4wMjEsLTAuNjAybC0xLjM0NSwtMS4yNTVjLTAuMTcyLC0wLjE2IC0wLjQ0MiwtMC4xNTEgLTAuNjAyLDAuMDIxYy0xLjM4MywxLjQ4MyAtMy4zMjEsMi4zMjYgLTUuMzQ5LDIuMzI2Yy00LjAzNywtMCAtNy4zMTQsLTMuMjc3IC03LjMxNCwtNy4zMTRjMCwtNC4wMzcgMy4yNzcsLTcuMzE0IDcuMzE0LC03LjMxNGMxLjM2LDAgMi42ODIsMC4zNzkgMy44MjQsMS4wODFaIi8+PC9zdmc+'; let _lastRenderer = null; const _position = new Vector2(); const _topLeft = new Vector2(); const _topRight = new Vector2(); const _botLeft = new Vector2(); const _botRight = new Vector2(); const _objectMatrix = new Matrix2(); const _rotateMatrix = new Matrix2(); const dragger = Object.assign(new Circle(10), { type: 'Resizer', selectable: false, focusable: false }); class ResizeHelper extends Box { static ALL = 0; static RESIZE = 1; static ROTATE = 2; constructor(objects, selectControls = null, radius = 5, tools = ResizeHelper.ALL) { if (!objects) return console.error(`ResizeHelper(): Missing 'objects' argument`); objects = Array.isArray(objects) ? objects : [ objects ]; if (objects.length === 0) return console.error(`ResizeHelper(): Objects array is empty`); super(); this.isHelper = true; this.type = 'ResizeHelper'; this.selectControls = selectControls; this.pointerEvents = true; this.draggable = true; this.focusable = true; this.selectable = false; this.lateUpdate = true; this.fillStyle = null; this.strokeStyle = null; // Layer let topLayer = 0; let bottomLayer = 0; for (const object of objects) { topLayer = Math.max(topLayer, object.layer + 1); bottomLayer = Math.max(bottomLayer, object.layer - 1); } this.layer = topLayer; // Background const background = Object.assign(new Box(), { pointerEvents: false, draggable: false, focusable: false, selectable: false }); background.isHelper = true; background.layer = bottomLayer; background.fillStyle.color = 'rgba(--icon-dark, 0.35)'; background.fillStyle.fallback = 'rgba(0, 85, 102, 0.35)'; background.strokeStyle.color = 'rgba(--icon-dark, 0.35)'; background.strokeStyle.fallback = 'rgba(0, 85, 102, 0.35)'; background.lineWidth = 2; background.constantWidth = true; background.visible = false; this.add(background); this.background = background; // Self const self = this; this.objects = objects; // Common Parent const commonAncestor = findCommonMostAncestor(objects); this.ghostParent = commonAncestor; // Initial Object Transforms const initialTransforms = {}; for (const object of objects) { initialTransforms[object.uuid] = { position: object.position.clone(), scale: object.scale.clone(), rotation: object.rotation, } } // Check for Same Rotation let firstRotation = MathUtils.equalizeAngle0to360(objects[0].rotation, false /* degrees? */); let sameRotation = true; for (const object of objects) { let nextRotation = MathUtils.equalizeAngle0to360(object.rotation, false /* degrees? */); sameRotation = sameRotation && MathUtils.fuzzyFloat(firstRotation, nextRotation, MathUtils.degreesToRadians(1)); } // World Box / Center const worldBox = new Box2(); let center; // Shared Rotation? if (sameRotation || objects.length === 1) { this.rotation = objects[0].rotation; worldBox.clear(); const rotationMatrix = new Matrix2().rotate(+this.rotation); const unRotateMatrix = new Matrix2().rotate(-this.rotation); // Unrotated World Boxes for (const object of objects) { const unRotatedPosition = unRotateMatrix.transformPoint(object.position); _objectMatrix.compose(unRotatedPosition.x, unRotatedPosition.y, object.scale.x, object.scale.y, 0, 0, 0); object.traverseVisible((child) => { const box = child.boundingBox; if (isFinite(box.min.x) && isFinite(box.min.y) && isFinite(box.max.x) && isFinite(box.max.y)) { _objectMatrix.applyToVector(_topLeft.copy(box.min)); _objectMatrix.applyToVector(_topRight.copy(box.max.x, box.min.y)); _objectMatrix.applyToVector(_botLeft.copy(box.min.x, box.max.y)); _objectMatrix.applyToVector(_botRight.copy(box.max)); const unrotatedBox = new Box2().setFromPoints(_topLeft, _topRight, _botLeft, _botRight); worldBox.union(unrotatedBox); } }); } // True Center const rotatedCenter = worldBox.getCenter(); center = rotationMatrix.transformPoint(rotatedCenter); // Find Initial Positions for (const object of objects) { const position = object.position.clone().sub(center); const initialPosition = unRotateMatrix.transformPoint(position).add(center); initialTransforms[object.uuid].position.copy(initialPosition); } // No Rotation } else { for (const object of objects) { worldBox.union(object.getWorldBoundingBox()); } center = worldBox.getCenter(); } // Starting Transform const halfSize = worldBox.getSize().multiplyScalar(0.5); this.position.copy(center); this.box.set(new Vector2(-halfSize.x, -halfSize.y), new Vector2(+halfSize.x, +halfSize.y)); this.computeBoundingBox(); this.updateMatrix(true); const startPosition = this.position.clone(); const startRotation = this.rotation; const startScale = this.scale.clone(); // Origin this.origin = new Vector2(); if (objects.length === 1) { objects[0].globalMatrix.applyToVector(this.origin); this.inverseGlobalMatrix.applyToVector(this.origin); } // Resizers let topLeft, topRight, bottomLeft, bottomRight; let topResizer, rightResizer, bottomResizer, leftResizer; let rotater, topLine, zeroLine, rotateLine; // Corners / Sides if (tools === ResizeHelper.ALL || tools === ResizeHelper.RESIZE) { function createResizer(name, x, y, type = 'box', addRotation, alpha, color) { let resizer; switch (type) { case 'circle': resizer = new Circle(); resizer.radius = radius; break; case 'line': resizer = new Line(); resizer.mouseBuffer = radius; break; case 'box': default: resizer = new Box(); resizer.box.set(new Vector2(-radius, -radius), new Vector2(radius, radius)); } resizer.name = name; resizer.type = 'Resizer'; resizer.isHelper = true; resizer.draggable = true; resizer.focusable = false; resizer.selectable = false; resizer.mouseBuffer = 5; resizer.layer = topLayer + 1; resizer.opacity = alpha; resizer.constantWidth = true; switch (type) { case 'box': case 'circle': if (color) { resizer.fillStyle = new ColorStyle(color); } else { resizer.fillStyle = new LinearGradientStyle(); resizer.fillStyle.start.set(-radius, -radius); resizer.fillStyle.end.set(radius, radius); resizer.fillStyle.addColorStop(0, '--icon-light'); resizer.fillStyle.addColorStop(1, '--icon-dark'); } resizer.strokeStyle.color = '--highlight'; resizer.lineWidth = OUTLINE_THICKNESS; break; case 'line': resizer.strokeStyle.color = '--highlight'; resizer.lineWidth = OUTLINE_THICKNESS; break; } resizer.cursor = function(camera) { const cursorStyles = [ { angle: 0, cursor: 'ew-resize' }, { angle: 45, cursor: 'nwse-resize' }, { angle: 90, cursor: 'ns-resize' }, { angle: 135, cursor: 'nesw-resize' }, { angle: 180, cursor: 'ew-resize' }, { angle: 225, cursor: 'nwse-resize' }, { angle: 270, cursor: 'ns-resize' }, { angle: 315, cursor: 'nesw-resize' }, { angle: 360, cursor: 'ew-resize' }, ]; let rotation = self.rotation; if (self.scale.x < 0 && self.scale.y > 0 || self.scale.x > 0 && self.scale.y < 0) { rotation -= (addRotation * (Math.PI / 180)); } else { rotation += (addRotation * (Math.PI / 180)); } rotation = (rotation + camera.rotation) * 180 / Math.PI; rotation = 360 - rotation; const normalizedRotation = MathUtils.equalizeAngle0to360(rotation, true /* degrees? */); let closestCursor = 'default'; let minAngleDiff = Infinity; for (const { angle, cursor } of cursorStyles) { const angleDiff = Math.abs(normalizedRotation - angle); if (angleDiff < minAngleDiff) { minAngleDiff = angleDiff; closestCursor = cursor; } } return closestCursor; }; let startDragPosition, startDragRotation, startDragScale; let startBox, worldPositionStart; resizer['onPointerDragStart'] = function(renderer) { // Starting Transform startBox = self.boundingBox.clone(); startDragPosition = self.position.clone(); startDragRotation = self.rotation; startDragScale = self.scale.clone(); // Setup Dragger self.ghostParent.add(dragger); dragger.type = 'Resizer'; dragger.resizeHelper = self; dragger['onPointerDragEnd'] = function(renderer) { dragger.destroy(); }; dragger['onPointerDrag'] = function(renderer) { Object2D.prototype.onPointerDrag.call(this, renderer); updateResizer(renderer); } dragger.setPosition = function(x, y) { Object2D.prototype.setPosition.call(this, x, y); updateResizer(); return dragger; } // Convert resizer world position to local position const worldPosition = resizer.getWorldPosition(); const parentPosition = self.ghostParent.inverseGlobalMatrix.transformPoint(worldPosition); dragger.position.copy(parentPosition); // Prepare Drag dragger.cursor = resizer.cursor; dragger.fillStyle = null; dragger.strokeStyle = null; dragger.pointerStartPosition = renderer.pointer.position.clone(); dragger.dragStartPosition = dragger.position.clone(); worldPositionStart = worldPosition.clone(); renderer.setDragObject(dragger); }; function updateResizer(renderer = _lastRenderer) { if (renderer) _lastRenderer = renderer; // Transform Delta const localPositionStart = self.inverseGlobalMatrix.transformPoint(worldPositionStart); const worldPositionEnd = dragger.getWorldPosition(); const localPositionEnd = self.inverseGlobalMatrix.transformPoint(worldPositionEnd); const delta = localPositionStart.clone().sub(localPositionEnd).multiply(self.scale); // Calculate Scale if (x === 0) delta.x = 0; if (y === 0) delta.y = 0; delta.multiplyScalar(0.5); const size = startBox.getSize(); const scaleX = MathUtils.sanity((x === 0) ? 0 : 2 / size.x); const scaleY = MathUtils.sanity((y === 0) ? 0 : 2 / size.y); // Maintain aspect ratio when Shift key is pressed if (renderer?.keyboard?.shiftPressed() && x !== 0 && y !== 0) { const aspectRatio = (size.x * startDragScale.x) / (size.y * startDragScale.y); if (Math.abs(aspectRatio) < Math.abs(delta.x / delta.y)) { delta.y = delta.x / aspectRatio; if (x !== y) delta.y *= -1; } else { delta.x = delta.y * aspectRatio; if (x !== y) delta.x *= -1; } } const scale = new Vector2(scaleX, scaleY); // Calculate offset between center of the bounding box and origin const positionOffset = new Vector2(); if (renderer?.keyboard?.ctrlPressed()) { positionOffset.x = ((self.origin.x - startBox.min.x) / size.x) * -2; positionOffset.y = ((self.origin.y - startBox.min.y) / size.y) * -2; if (x > 0) positionOffset.x = -2 - positionOffset.x; if (y > 0) positionOffset.y = -2 - positionOffset.y; positionOffset.multiply(delta); } else { positionOffset.copy(startBox.getCenter()); positionOffset.multiply(delta).multiply(scale).multiply(x, y); } // Apply the rotation to the delta & position offset const rotationMatrix = new Matrix2().rotate(startDragRotation); const rotatedDelta = rotationMatrix.transformPoint(delta); const rotatedPositionOffset = rotationMatrix.transformPoint(positionOffset); // Update the tool's position self.position.copy(startDragPosition).sub(rotatedDelta).sub(rotatedPositionOffset); // Update the tool's scale delta.multiply(x, y).multiply(scale); self.scale.copy(startDragScale).add(delta); self.scale.x = MathUtils.noZero(MathUtils.sanity(self.scale.x)); self.scale.y = MathUtils.noZero(MathUtils.sanity(self.scale.y)); self.updateMatrix(true); // Update individual objects for (const object of objects) { const initialTransform = initialTransforms[object.uuid]; const rotatedScale = self.scale.clone(); let initialRotation = initialTransform.rotation - startRotation; initialRotation = MathUtils.equalizeAngle0to360(initialRotation, false); // Rotate Scale? const fortyFive = Math.PI / 4; let flip = false; if (initialRotation < fortyFive * 1) flip = false; else if (initialRotation < fortyFive * 3) flip = true; else if (initialRotation < fortyFive * 5) flip = false; else if (initialRotation < fortyFive * 7) flip = true; else flip = false; if (flip) { const sx = Math.sign(rotatedScale.x); const sy = Math.sign(rotatedScale.y); rotatedScale.x = Math.abs(self.scale.y) * sx; rotatedScale.y = Math.abs(self.scale.x) * sy; } object.scale.copy(initialTransform.scale).multiply(rotatedScale); object.scale.x = MathUtils.noZero(MathUtils.sanity(object.scale.x)); object.scale.y = MathUtils.noZero(MathUtils.sanity(object.scale.y)); } updateObjects(renderer, false /* lerp */); } return resizer; } topRight = createResizer('Top Right', -1, -1, 'box', 45, 1);//, 'yellow'); topLeft = createResizer('Top Left', 1, -1, 'box', 135, 1);//, 'blue'); bottomLeft = createResizer('Bottom Left', 1, 1, 'box', 225, 1);//, 'green'); bottomRight = createResizer('Bottom Right', -1, 1, 'box', 315, 1);//, 'red'); rightResizer = createResizer('Right', -1, 0, 'line', 0, 1); topResizer = createResizer('Top', 0, -1, 'line', 90, 1); leftResizer = createResizer('Left', 1, 0, 'line', 180, 1); bottomResizer = createResizer('Bottom', 0, 1, 'line', 270, 1); this.add(bottomRight, bottomLeft, topLeft, topRight); this.add(rightResizer, bottomResizer, leftResizer, topResizer); } // Rotate Tool if (tools === ResizeHelper.ALL || tools === ResizeHelper.ROTATE) { // Circle rotater = Object.assign(new Circle(), { draggable: true, focusable: false, selectable: false }); rotater.type = 'Rotater'; rotater.isHelper = true; rotater.resizeHelper = self; rotater.radius = radius + 1; rotater.mouseBuffer = 5; rotater.layer = topLayer + 2; rotater.lineWidth = OUTLINE_THICKNESS; rotater.constantWidth = true; rotater.fillStyle = new LinearGradientStyle(); rotater.fillStyle.start.set(-radius, -radius); rotater.fillStyle.end.set(radius, radius); rotater.fillStyle.addColorStop(0, '--icon-light'); rotater.fillStyle.addColorStop(1, '--icon-dark'); rotater.strokeStyle.color = '--highlight'; rotater.cursor = `url('${CURSOR_ROTATE}') 16 16, auto`; let rotating = false, rotaterAngle = 0, rotaterStart = 0; let rotationStart = new Vector2(); let rotationOrigin = new Vector2(); rotater['onPointerDragStart'] = function(renderer) { rotaterAngle = self.rotation; rotaterStart = self.rotation; rotationStart = self.globalMatrix.transformPoint(new Vector2(0, 0)); rotationOrigin = self.globalMatrix.transformPoint(self.origin); rotating = true; }; rotater['onPointerDragEnd'] = function(renderer) { rotating = false; }; rotater.onPointerDrag = function(renderer) { Object2D.prototype.onPointerDrag.call(this, renderer); if (renderer) _lastRenderer = renderer; if (rotater.isDragging) { const pointer = renderer.pointer; const camera = renderer.camera; const pointerStart = pointer.position.clone(); const pointerEnd = pointer.position.clone().sub(pointer.delta.x, pointer.delta.y * -1); const worldPositionStart = renderer.screenToWorld(pointerStart); const localPositionStart = self.inverseGlobalMatrix.transformPoint(worldPositionStart); const worldPositionEnd = renderer.screenToWorld(pointerEnd); const localPositionEnd = self.inverseGlobalMatrix.transformPoint(worldPositionEnd); localPositionStart.sub(self.origin).multiply(self.scale); localPositionEnd.sub(self.origin).multiply(self.scale); const angle = localPositionEnd.angleBetween(localPositionStart); const cross = localPositionEnd.cross(localPositionStart); const sign = Math.sign(cross); rotaterAngle += (angle * sign); while (rotaterAngle < Math.PI * -2) { rotaterAngle += Math.PI * 2; } while (rotaterAngle > Math.PI * +2) { rotaterAngle -= Math.PI * 2; } rotater.setRotation(rotaterAngle); } }; // Override set position to update objects during snap to grid rotater.setRotation = function(rad) { Object2D.prototype.setRotation.call(this, rad); // Set Rotation self.rotation = rad; // Rotate Around Origin if (rotating) { const rotateMatrix = new Matrix2().rotate(rad - rotaterStart); const worldPosition = rotationStart.clone(); worldPosition.sub(rotationOrigin); rotateMatrix.applyToVector(worldPosition); worldPosition.add(rotationOrigin); const parentPosition = self.ghostParent.inverseGlobalMatrix.transformPoint(worldPosition); self.position.copy(parentPosition); } // Update Objects updateObjects(null, false /* lerp */); return self; } // Top Line topLine = Object.assign(new Line(), { draggable: false, focusable: false, selectable: false }); topLine.layer = topLayer + 1; topLine.lineWidth = OUTLINE_THICKNESS; topLine.constantWidth = true; topLine.strokeStyle.color = '--highlight'; // Zero ° Line zeroLine = Object.assign(new Line(), { draggable: false, focusable: false, selectable: false }); zeroLine.layer = topLayer + 1; zeroLine.lineWidth = OUTLINE_THICKNESS; zeroLine.constantWidth = true; zeroLine.strokeStyle.color = '--highlight'; // Rotate ° Line rotateLine = Object.assign(new Line(), { draggable: false, focusable: false, selectable: false }); rotateLine.layer = topLayer + 1; rotateLine.lineWidth = OUTLINE_THICKNESS; rotateLine.constantWidth = true; rotateLine.strokeStyle.color = '--highlight'; // Add to Rotater this.add(rotater, topLine, zeroLine, rotateLine); } // Overrides to update positions on Drag this.onPointerDrag = function(renderer) { Object2D.prototype.onPointerDrag.call(this, renderer); if (self.isDragging) updateObjects(renderer, true /* lerp */); }; this.onPointerDragEnd = function(renderer) { if (self.isDragging) updateObjects(renderer, false /* lerp */); }; // Override set position to update objects during snap to grid this.setPosition = function(x, y) { Object2D.prototype.setPosition.call(this, x, y); updateObjects(null, false /* lerp */); return self; } // Update object's position based on the scaled relative position function updateObjects(renderer = _lastRenderer, lerp = true) { if (!renderer && !_lastRenderer) return; if (renderer) _lastRenderer = renderer; for (const object of objects) { const initialPosition = initialTransforms[object.uuid].position; const initialRotation = initialTransforms[object.uuid].rotation; const initialScale = initialTransforms[object.uuid].scale; // Rotation object.rotation = initialRotation + (self.rotation - startRotation); // Position const relativePosition = initialPosition.clone().sub(startPosition); const scaledPosition = relativePosition.clone().multiply(self.scale); const rotateAngle = (object.rotation - initialRotation) + startRotation; const rotationMatrix = new Matrix2().rotate(rotateAngle); const rotatedPosition = rotationMatrix.transformPoint(scaledPosition); _position.copy(rotatedPosition).add(self.position); // Lerp? if (lerp && self.isDragging) { object.position.smoothstep(_position, renderer.deltaTime * 30); // delta time ~ 0.0167 (1 / 60) } else { object.position.copy(_position); } // Flipped? const wasSame = Math.sign(object.scale.x) === Math.sign(object.scale.y); const isSame = Math.sign(initialScale.x) === Math.sign(initialScale.y); if (wasSame !== isSame) { object.rotation -= self.rotation; object.rotation *= -1; object.rotation += self.rotation; } // Update Matrix object.traverse((child) => { child.updateMatrix(true); }); } self.onUpdate(renderer); // Emit Event if (self.selectControls) self.selectControls.transformed = true; } // Update this.onUpdate = function(renderer) { if (!renderer) return; const camera = renderer.camera; const showResizers = !self.isDragging; const worldPosition = self.globalMatrix.getPosition(); const worldRotation = self.globalMatrix.getRotation(); const worldScale = self.globalMatrix.getScale(); // Background if (self.background) { self.background.box.set(new Vector2(-halfSize.x, -halfSize.y), new Vector2(+halfSize.x, +halfSize.y)); self.background.updateMatrix(true); self.background.visible = true; } // Rotate Tool const handleOffset = ((radius * 4) / Math.abs(worldScale.y)) / camera.scale; const topCenterWorld = new Vector2(0, halfSize.y); const topCenterWorldOffset = new Vector2(0, halfSize.y + handleOffset); if (rotater) { rotater.position.copy(topCenterWorldOffset); rotater.rotation = 0; rotater.scale.set(1 / worldScale.x, 1 / worldScale.y).divideScalar(camera.scale); rotater.updateMatrix(true); rotater.visible = showResizers; } if (topLine) { topLine.from.copy(topCenterWorldOffset); topLine.to.copy(topCenterWorld); topLine.updateMatrix(true); topLine.visible = showResizers; } if (zeroLine) { zeroLine.from.set(self.origin.x, self.origin.y); zeroLine.to.set(self.origin.x, self.origin.y + (handleOffset * 1.5)); zeroLine.updateMatrix(true); zeroLine.visible = rotater.isDragging; } if (rotateLine) { rotateLine.from.set(self.origin.x, self.origin.y); rotateLine.to.set(0, radius * 4 * 1.5).add(worldPosition); self.inverseGlobalMatrix.applyToVector(rotateLine.to); rotateLine.to.divideScalar(camera.scale); rotateLine.to.add(self.origin); rotateLine.updateMatrix(true); rotateLine.visible = rotater.isDragging; } // Corner Resizers function updateResizer(resizer, x, y, type) { if (!resizer) return; resizer.position.set(x, y); if (type === 'v') { resizer.from.set(0, -halfSize.y); resizer.to.set(0, +halfSize.y); } else if (type === 'h') { resizer.from.set(-halfSize.x, 0); resizer.to.set(+halfSize.x, 0); } else { resizer.scale.set((1 / worldScale.x) / camera.scale, (1 / worldScale.y) / camera.scale); } resizer.updateMatrix(true); resizer.visible = showResizers; } updateResizer(topLeft, -halfSize.x, +halfSize.y); updateResizer(topRight, +halfSize.x, +halfSize.y); updateResizer(bottomLeft, -halfSize.x, -halfSize.y); updateResizer(bottomRight, +halfSize.x, -halfSize.y); updateResizer(leftResizer, -halfSize.x, 0, 'v'); updateResizer(rightResizer, +halfSize.x, 0, 'v'); updateResizer(topResizer, 0, +halfSize.y, 'h'); updateResizer(bottomResizer, 0, -halfSize.y, 'h'); }; /***** HELPER FUNCTIONS *****/ this.flipVertical = function(renderer) { self.scale.y *= -1; // // TODO: Update individual object scales (see resizer) // updateObjects(renderer, false /* lerp */); return self; }; } } export { ResizeHelper }; /******************** INTERNAL ********************/ function findCommonMostAncestor(objects) { if (objects.length === 0) return null; if (objects.length === 1) return objects[0].parent; function getAncestors(object) { const ancestors = []; let currentObject = object; while (currentObject.parent) { ancestors.unshift(currentObject.parent); currentObject = currentObject.parent; } return ancestors; } const ancestors = objects.map(getAncestors); const minLength = Math.min(...ancestors.map(arr => arr.length)); for (let i = 0; i < minLength; i++) { const ancestor = ancestors[0][i]; for (let j = 1; j < ancestors.length; j++) { if (ancestors[j][i] !== ancestor) { return ancestor.parent; } } } return ancestors[0][minLength - 1]; }