Fix rotation point and add migration for old files
This commit is contained in:
parent
df32b43915
commit
f610ef733d
101
src/main.js
101
src/main.js
|
|
@ -3,7 +3,7 @@ import * as fitCurve from '/fit-curve.js';
|
||||||
import { Bezier } from "/bezier.js";
|
import { Bezier } from "/bezier.js";
|
||||||
import { Quadtree } from './quadtree.js';
|
import { Quadtree } from './quadtree.js';
|
||||||
import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.js';
|
import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.js';
|
||||||
import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerpColor, lerp, camelToWords, generateWaveform, floodFillRegion, getShapeAtPoint, hslToRgb, drawCheckerboardBackground, hexToHsl, hsvToRgb, hexToHsv, rgbToHex, clamp, drawBorderedRect, drawCenteredText, drawHorizontallyCenteredText, deepMerge, getPointNearBox, arraysAreEqual, drawRegularPolygon, getFileExtension, createModal, deeploop, signedAngleBetweenVectors } from './utils.js';
|
import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerpColor, lerp, camelToWords, generateWaveform, floodFillRegion, getShapeAtPoint, hslToRgb, drawCheckerboardBackground, hexToHsl, hsvToRgb, hexToHsv, rgbToHex, clamp, drawBorderedRect, drawCenteredText, drawHorizontallyCenteredText, deepMerge, getPointNearBox, arraysAreEqual, drawRegularPolygon, getFileExtension, createModal, deeploop, signedAngleBetweenVectors, rotateAroundPoint, getRotatedBoundingBox, rotateAroundPointIncremental } from './utils.js';
|
||||||
import { backgroundColor, darkMode, foregroundColor, frameWidth, gutterHeight, highlight, iconSize, triangleSize, labelColor, layerHeight, layerWidth, scrubberColor, shade, shadow } from './styles.js';
|
import { backgroundColor, darkMode, foregroundColor, frameWidth, gutterHeight, highlight, iconSize, triangleSize, labelColor, layerHeight, layerWidth, scrubberColor, shade, shadow } from './styles.js';
|
||||||
import { Icon } from './icon.js';
|
import { Icon } from './icon.js';
|
||||||
const { writeTextFile: writeTextFile, readTextFile: readTextFile, writeFile: writeFile, readFile: readFile }= window.__TAURI__.fs;
|
const { writeTextFile: writeTextFile, readTextFile: readTextFile, writeFile: writeFile, readFile: readFile }= window.__TAURI__.fs;
|
||||||
|
|
@ -772,6 +772,7 @@ let actions = {
|
||||||
editFrame: {
|
editFrame: {
|
||||||
create: (frame) => {
|
create: (frame) => {
|
||||||
redoStack.length = 0; // Clear redo stack
|
redoStack.length = 0; // Clear redo stack
|
||||||
|
console.log(frame.idx in startProps)
|
||||||
if (!(frame.idx in startProps)) return;
|
if (!(frame.idx in startProps)) return;
|
||||||
let action = {
|
let action = {
|
||||||
newState: structuredClone(frame.keys),
|
newState: structuredClone(frame.keys),
|
||||||
|
|
@ -1036,11 +1037,23 @@ let actions = {
|
||||||
redoStack.length = 0
|
redoStack.length = 0
|
||||||
let serializableShapes = []
|
let serializableShapes = []
|
||||||
let serializableObjects = []
|
let serializableObjects = []
|
||||||
|
let bbox;
|
||||||
for (let shape of context.shapeselection) {
|
for (let shape of context.shapeselection) {
|
||||||
serializableShapes.push(shape.idx)
|
serializableShapes.push(shape.idx)
|
||||||
|
if (bbox==undefined) {
|
||||||
|
bbox = shape.bbox()
|
||||||
|
} else {
|
||||||
|
growBoundingBox(bbox, shape.bbox())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (let object of context.selection) {
|
for (let object of context.selection) {
|
||||||
serializableObjects.push(object.idx)
|
serializableObjects.push(object.idx)
|
||||||
|
// TODO: rotated bbox
|
||||||
|
if (bbox==undefined) {
|
||||||
|
bbox = object.bbox()
|
||||||
|
} else {
|
||||||
|
growBoundingBox(bbox, object.bbox())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
context.shapeselection = []
|
context.shapeselection = []
|
||||||
context.selection = []
|
context.selection = []
|
||||||
|
|
@ -1049,7 +1062,8 @@ let actions = {
|
||||||
objects: serializableObjects,
|
objects: serializableObjects,
|
||||||
groupUuid: uuidv4(),
|
groupUuid: uuidv4(),
|
||||||
parent: context.activeObject.idx,
|
parent: context.activeObject.idx,
|
||||||
frame: context.activeObject.currentFrame.idx
|
frame: context.activeObject.currentFrame.idx,
|
||||||
|
position: {x: (bbox.x.min+bbox.x.max)/2, y: (bbox.y.min+bbox.y.max)/2}
|
||||||
}
|
}
|
||||||
undoStack.push({name: 'group', action: action})
|
undoStack.push({name: 'group', action: action})
|
||||||
actions.group.execute(action)
|
actions.group.execute(action)
|
||||||
|
|
@ -1062,15 +1076,16 @@ let actions = {
|
||||||
let frame = action.frame ? pointerList[action.frame] : parent.currentFrame
|
let frame = action.frame ? pointerList[action.frame] : parent.currentFrame
|
||||||
for (let shapeIdx of action.shapes) {
|
for (let shapeIdx of action.shapes) {
|
||||||
let shape = pointerList[shapeIdx]
|
let shape = pointerList[shapeIdx]
|
||||||
|
shape.translate(-action.position.x, -action.position.y)
|
||||||
group.currentFrame.addShape(shape)
|
group.currentFrame.addShape(shape)
|
||||||
frame.removeShape(shape)
|
frame.removeShape(shape)
|
||||||
}
|
}
|
||||||
for (let objectIdx of action.objects) {
|
for (let objectIdx of action.objects) {
|
||||||
let object = pointerList[objectIdx]
|
let object = pointerList[objectIdx]
|
||||||
group.addObject(object, object.x, object.y)
|
group.addObject(object, object.x - position.x, object.y - position.y)
|
||||||
parent.removeChild(object)
|
parent.removeChild(object)
|
||||||
}
|
}
|
||||||
parent.addObject(group)
|
parent.addObject(group, action.position.x, action.position.y)
|
||||||
if (context.activeObject==parent && context.selection.length==0 && context.shapeselection.length==0) {
|
if (context.activeObject==parent && context.selection.length==0 && context.shapeselection.length==0) {
|
||||||
context.selection.push(group)
|
context.selection.push(group)
|
||||||
}
|
}
|
||||||
|
|
@ -2011,7 +2026,11 @@ class BaseShape {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recalculateBoundingBox() {
|
recalculateBoundingBox() {
|
||||||
|
this.boundingBox = undefined
|
||||||
for (let curve of this.curves) {
|
for (let curve of this.curves) {
|
||||||
|
if (!this.boundingBox) {
|
||||||
|
this.boundingBox = curve.bbox();
|
||||||
|
}
|
||||||
growBoundingBox(this.boundingBox, curve.bbox())
|
growBoundingBox(this.boundingBox, curve.bbox())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2365,6 +2384,15 @@ class Shape extends BaseShape {
|
||||||
const pointsSorted = angles.sort((a, b) => a.angle - b.angle);
|
const pointsSorted = angles.sort((a, b) => a.angle - b.angle);
|
||||||
return pointsSorted
|
return pointsSorted
|
||||||
}
|
}
|
||||||
|
translate(x, y) {
|
||||||
|
for (let curve of this.curves) {
|
||||||
|
for (let i in curve.points) {
|
||||||
|
const point = curve.points[i]
|
||||||
|
curve.points[i] = {x: point.x + x, y: point.y + y}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.update()
|
||||||
|
}
|
||||||
updateVertices() {
|
updateVertices() {
|
||||||
this.vertices = []
|
this.vertices = []
|
||||||
let utils = Bezier.getUtils()
|
let utils = Bezier.getUtils()
|
||||||
|
|
@ -2728,9 +2756,9 @@ class GraphicsObject {
|
||||||
let bbox = undefined;
|
let bbox = undefined;
|
||||||
for (let item of context.selection) {
|
for (let item of context.selection) {
|
||||||
if (bbox==undefined) {
|
if (bbox==undefined) {
|
||||||
bbox = structuredClone(item.bbox())
|
bbox = getRotatedBoundingBox(item, debugPoints)
|
||||||
} else {
|
} else {
|
||||||
growBoundingBox(bbox, item.bbox())
|
growBoundingBox(bbox, getRotatedBoundingBox(item, debugPoints))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bbox != undefined) {
|
if (bbox != undefined) {
|
||||||
|
|
@ -3039,8 +3067,11 @@ async function _save(path) {
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
for (let action of undoStack) {
|
||||||
|
console.log(action.name)
|
||||||
|
}
|
||||||
const fileData = {
|
const fileData = {
|
||||||
version: "1.5",
|
version: "1.6",
|
||||||
width: config.fileWidth,
|
width: config.fileWidth,
|
||||||
height: config.fileHeight,
|
height: config.fileHeight,
|
||||||
fps: config.framerate,
|
fps: config.framerate,
|
||||||
|
|
@ -3109,15 +3140,55 @@ async function _open(path, returnJson=false) {
|
||||||
await messageDialog("File has no content!", {title: "Parse error", kind: 'error'})
|
await messageDialog("File has no content!", {title: "Parse error", kind: 'error'})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const objectOffsets = {}
|
||||||
for (let action of file.actions) {
|
for (let action of file.actions) {
|
||||||
if (!(action.name in actions)) {
|
if (!(action.name in actions)) {
|
||||||
await messageDialog(`Invalid action ${action.name}. File may be corrupt.`, { title: "Error", kind: 'error'})
|
await messageDialog(`Invalid action ${action.name}. File may be corrupt.`, { title: "Error", kind: 'error'})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(action.name)
|
console.log(action.name)
|
||||||
|
// Data fixes
|
||||||
|
if (file.version <= "1.5") {
|
||||||
|
// Fix coordinates of objects
|
||||||
|
if (action.name=="group") {
|
||||||
|
let bbox;
|
||||||
|
for (let i of action.action.shapes) {
|
||||||
|
const shape = pointerList[i]
|
||||||
|
if (bbox==undefined) {
|
||||||
|
bbox = shape.bbox()
|
||||||
|
} else {
|
||||||
|
growBoundingBox(bbox, shape.bbox())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i of action.action.objects) {
|
||||||
|
const object = pointerList[i]// TODO: rotated bbox
|
||||||
|
if (bbox==undefined) {
|
||||||
|
bbox = object.bbox()
|
||||||
|
} else {
|
||||||
|
growBoundingBox(bbox, object.bbox())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const position = {x: (bbox.x.min+bbox.x.max)/2, y: (bbox.y.min+bbox.y.max)/2}
|
||||||
|
action.action.position = position
|
||||||
|
objectOffsets[action.action.groupUuid] = position
|
||||||
|
} else if (action.name=="editFrame") {
|
||||||
|
for (let key in action.action.newState) {
|
||||||
|
if (key in objectOffsets) {
|
||||||
|
action.action.newState[key].x += objectOffsets[key].x
|
||||||
|
action.action.newState[key].y += objectOffsets[key].y
|
||||||
|
action.action.oldState[key].x += objectOffsets[key].x
|
||||||
|
action.action.oldState[key].y += objectOffsets[key].y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await actions[action.name].execute(action.action)
|
await actions[action.name].execute(action.action)
|
||||||
undoStack.push(action)
|
undoStack.push(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSaveIndex = undoStack.length;
|
lastSaveIndex = undoStack.length;
|
||||||
filePath = path
|
filePath = path
|
||||||
// Tauri thinks it is setting the title here, but it isn't getting updated
|
// Tauri thinks it is setting the title here, but it isn't getting updated
|
||||||
|
|
@ -3776,6 +3847,7 @@ function stage() {
|
||||||
selection: structuredClone(selection)
|
selection: structuredClone(selection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
context.activeObject.currentFrame.saveState()
|
||||||
} else {
|
} else {
|
||||||
stage.style.cursor = "default"
|
stage.style.cursor = "default"
|
||||||
}
|
}
|
||||||
|
|
@ -4077,9 +4149,9 @@ function stage() {
|
||||||
let bbox = undefined;
|
let bbox = undefined;
|
||||||
for (let item of context.selection) {
|
for (let item of context.selection) {
|
||||||
if (bbox==undefined) {
|
if (bbox==undefined) {
|
||||||
bbox = structuredClone(item.bbox())
|
bbox = getRotatedBoundingBox(item)
|
||||||
} else {
|
} else {
|
||||||
growBoundingBox(bbox, item.bbox())
|
growBoundingBox(bbox, getRotatedBoundingBox(item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bbox==undefined) break;
|
if (bbox==undefined) break;
|
||||||
|
|
@ -4108,16 +4180,21 @@ function stage() {
|
||||||
} else if (context.dragDirection.indexOf('e') != -1) {
|
} else if (context.dragDirection.indexOf('e') != -1) {
|
||||||
current.x.max = mouse.x
|
current.x.max = mouse.x
|
||||||
}
|
}
|
||||||
|
// Calculate the translation difference between current and initial values
|
||||||
|
let delta_x = current.x.min - initial.x.min;
|
||||||
|
let delta_y = current.y.min - initial.y.min;
|
||||||
|
|
||||||
if (context.dragDirection == 'r') {
|
if (context.dragDirection == 'r') {
|
||||||
let pivot = {
|
let pivot = {
|
||||||
x: (initial.x.min+initial.x.max)/2,
|
x: (initial.x.min+initial.x.max)/2,
|
||||||
y: (initial.y.min+initial.y.max)/2,
|
y: (initial.y.min+initial.y.max)/2,
|
||||||
}
|
}
|
||||||
current.rotation = signedAngleBetweenVectors(pivot, initial.mouse, mouse)
|
current.rotation = signedAngleBetweenVectors(pivot, initial.mouse, mouse)
|
||||||
|
const {dx, dy} = rotateAroundPointIncremental(current.x.min, current.y.min, pivot, current.rotation)
|
||||||
|
// delta_x -= dx
|
||||||
|
// delta_y -= dy
|
||||||
|
// console.log(dx, dy)
|
||||||
}
|
}
|
||||||
// Calculate the translation difference between current and initial values
|
|
||||||
const delta_x = current.x.min - initial.x.min;
|
|
||||||
const delta_y = current.y.min - initial.y.min;
|
|
||||||
|
|
||||||
// This is probably unnecessary since initial rotation is 0
|
// This is probably unnecessary since initial rotation is 0
|
||||||
const delta_rot = current.rotation - initial.rotation
|
const delta_rot = current.rotation - initial.rotation
|
||||||
|
|
|
||||||
64
src/utils.js
64
src/utils.js
|
|
@ -581,6 +581,67 @@ function signedAngleBetweenVectors(a, b, c) {
|
||||||
return signedAngle;
|
return signedAngle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rotateAroundPointIncremental(x, y, point, angle) {
|
||||||
|
const { x: newX, y: newY } = rotateAroundPoint(x, y, point, angle)
|
||||||
|
const dx = newX - x
|
||||||
|
const dy = newY - y
|
||||||
|
return { dx, dy }
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateAroundPoint(x, y, point, angle) {
|
||||||
|
const dx = x - point.x;
|
||||||
|
const dy = y - point.y;
|
||||||
|
const cosAngle = Math.cos(angle);
|
||||||
|
const sinAngle = Math.sin(angle);
|
||||||
|
|
||||||
|
const rotatedX = point.x + (dx * cosAngle - dy * sinAngle);
|
||||||
|
const rotatedY = point.y + (dx * sinAngle + dy * cosAngle);
|
||||||
|
|
||||||
|
return { x: rotatedX, y: rotatedY };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRotatedBoundingBox(object, debugPoints=[]) {
|
||||||
|
const bbox = object.bbox(); // Get the bounding box of the object without transformation
|
||||||
|
|
||||||
|
const { x: { min: xMin, max: xMax }, y: { min: yMin, max: yMax } } = bbox;
|
||||||
|
|
||||||
|
// Calculate the four corners of the bounding box
|
||||||
|
const corners = [
|
||||||
|
{ x: xMin, y: yMin }, // Bottom-left
|
||||||
|
{ x: xMax, y: yMin }, // Bottom-right
|
||||||
|
{ x: xMin, y: yMax }, // Top-left
|
||||||
|
{ x: xMax, y: yMax } // Top-right
|
||||||
|
];
|
||||||
|
|
||||||
|
const center = {
|
||||||
|
x: (xMin + xMax) / 2,
|
||||||
|
y: (yMin + yMax) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate each corner and track the min/max x and y values
|
||||||
|
let rotatedCorners = corners.map(corner => {
|
||||||
|
return rotateAroundPoint(corner.x, corner.y, center, object.rotation);
|
||||||
|
});
|
||||||
|
|
||||||
|
debugPoints.length = 0
|
||||||
|
for (let corner of rotatedCorners) {
|
||||||
|
debugPoints.push(corner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the new bounding box after rotation
|
||||||
|
let rotatedXMin = Math.min(...rotatedCorners.map(corner => corner.x));
|
||||||
|
let rotatedXMax = Math.max(...rotatedCorners.map(corner => corner.x));
|
||||||
|
let rotatedYMin = Math.min(...rotatedCorners.map(corner => corner.y));
|
||||||
|
let rotatedYMax = Math.max(...rotatedCorners.map(corner => corner.y));
|
||||||
|
|
||||||
|
// Return the new bounding box with min/max x and y values
|
||||||
|
return {
|
||||||
|
x: { min: rotatedXMin, max: rotatedXMax },
|
||||||
|
y: { min: rotatedYMin, max: rotatedYMax }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function drawBorderedRect(ctx, x, y, width, height, top, bottom, left, right) {
|
function drawBorderedRect(ctx, x, y, width, height, top, bottom, left, right) {
|
||||||
ctx.fillRect(x, y, width, height)
|
ctx.fillRect(x, y, width, height)
|
||||||
if (top) {
|
if (top) {
|
||||||
|
|
@ -832,6 +893,9 @@ export {
|
||||||
drawCheckerboardBackground,
|
drawCheckerboardBackground,
|
||||||
clamp,
|
clamp,
|
||||||
signedAngleBetweenVectors,
|
signedAngleBetweenVectors,
|
||||||
|
rotateAroundPoint,
|
||||||
|
rotateAroundPointIncremental,
|
||||||
|
getRotatedBoundingBox,
|
||||||
drawBorderedRect,
|
drawBorderedRect,
|
||||||
drawCenteredText,
|
drawCenteredText,
|
||||||
drawHorizontallyCenteredText,
|
drawHorizontallyCenteredText,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue