Skip to content

Instantly share code, notes, and snippets.

@mrsln
Created May 12, 2020 11:36
Show Gist options
  • Save mrsln/17ca3865b2e461b55afe506a45d39315 to your computer and use it in GitHub Desktop.
Save mrsln/17ca3865b2e461b55afe506a45d39315 to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
const AppMachine = Machine(
{
id: "appmachine",
type: "parallel",
context: {
template: {},
param: {},
rect: {},
},
states: {
rect: {
idle: {
on: {
RESIZE: "resizing",
MOVE: "moving",
},
states: {
resizing: {},
moving: {},
},
},
},
app: {
states: {
loading: {
after: {
LOADING_TIMEOUT: "failure.timeout",
},
on: {
RESOLVE: "loaded",
REJECT: "failure",
},
},
loaded: {
on: {
UPDATE_SETTINGS: {
internal: true,
actions: assign({
param: (event) => event.param,
}),
},
},
},
failure: {
initial: "rejection",
states: {
rejection: {
meta: {
errorMessage: "Loading the app has failed.",
},
},
timeout: {
meta: {
errorMessage: "Loading the app has timed out.",
},
},
},
meta: {
error: true,
},
},
},
},
},
},
{
delays: {
LOADING_TIMEOUT: 10000,
},
}
);
const DraggableMachine = Machine({
id: "draggable",
initial: "dragging",
context: {
dx: 0,
dy: 0,
startX: 0,
startY: 0,
},
states: {
dragging: {
on: {
mousemove: {
actions: sendParent((context, event) => ({
type: "UPDATE_SELECTION",
dx: context.startX - event.x,
dy: context.startY - event.y,
})),
},
mouseup: {
target: "finish",
},
},
},
finish: {
type: "final",
},
},
});
const SettingsStates = {
id: "settings",
initial: "loading",
states: {
loading: {
after: {
LOADING_TIMEOUT: "failure.timeout",
},
on: {
RESOLVE: "loaded",
REJECT: "failure",
},
},
loaded: {
on: {
UPDATE: {
actions: ["updateSelectedAppSettings"],
},
},
},
failure: {
initial: "rejection",
on: {
RETRY: "loading",
},
states: {
rejection: {
meta: {
errorMessage: "Loading settings has failed.",
},
},
timeout: {
meta: {
errorMessage: "Loading settings has timed out.",
},
},
},
meta: {
error: true,
},
},
},
};
const MIN_SIZE = 0.01;
const StateMachine = Machine(
{
id: "studio",
initial: "idle",
context: {
selection: {
x: 0.01,
y: 0.01,
startX: 0,
startY: 0,
width: 0.4,
height: 0.4,
resizingPart: "bottomRightCorner",
appId: "",
},
draggingItem: {
offsetX: 0,
offsetY: 0,
startX: 0,
startY: 0,
x: 0.01,
y: 0.01,
app: {},
},
appOverlay: {
x: 0,
y: 0,
width: 0,
height: 0,
},
apps: [],
},
states: {
idle: {
on: {
mousedown: {
target: "resizing",
actions: "startResizing",
},
mousedownOnMedia: {
target: "draggingMedia",
actions: "startDraggingMedia",
},
canvasMouseDown: [
{
cond: "isWithinApp",
actions: ["selectApp", "startResizing"],
target: "resizing",
},
{
cond: "isNotWithinApp",
target: ".idle",
},
],
mousemove: [
{ cond: "isWithinApp", actions: "displayAppOverlay" },
{ cond: "isNotWithinApp", actions: "hideAppOverlay" },
],
deleteApp: {
actions: ["deleteApp", "hideAppOverlay"],
target: ".idle",
},
},
initial: "idle",
states: {
idle: {
entry: "reset",
},
selected: {
internal: true,
...SettingsStates,
},
},
},
resizing: {
invoke: {
id: "drag",
src: DraggableMachine,
autoForward: true,
onDone: { target: "idle.selected", actions: "updateAppOverlay" },
data: {
startX: (_context, event) => event.x,
startY: (_context, event) => event.y,
},
},
on: {
UPDATE_SELECTION: {
actions: ["resize", "updateSelectedApp", "hideAppOverlay"],
},
},
},
draggingMedia: {
invoke: {
id: "drag",
src: DraggableMachine,
autoForward: true,
onDone: {
target: "idle.selected",
actions: ["addApp", "selectLatestApp"],
},
data: {
startX: (context) => context.draggingItem.startX,
startY: (context) => context.draggingItem.startY,
},
},
on: {
UPDATE_SELECTION: {
actions: "draggingMedia",
},
},
},
},
},
{
delays: { LOADING_TIMEOUT: 10000 },
guards: {
isWithinApp: (context, event) =>
!!getAppUnderMouse(context.apps, event.x, event.y),
isNotWithinApp: (context, event) =>
!getAppUnderMouse(context.apps, event.x, event.y),
},
actions: {
startResizing: assign((context, event) => {
return {
...context,
selection: {
...context.selection,
resizingPart: event.resizingPart,
startX: context.selection.x,
startY: context.selection.y,
startWidth: context.selection.width,
startHeight: context.selection.height,
},
};
}),
startDraggingMedia: assign((context, event) => {
return {
...context,
draggingItem: {
...context.draggingItem,
startX: event.x,
startY: event.y,
x: event.x - event.offsetX,
y: event.y - event.offsetY,
offsetX: event.offsetX,
offsetY: event.offsetY,
app: event.app,
},
};
}),
reset: assign((context) => ({
...context,
selection: {
x: 0.02,
y: 0.02,
startX: 0,
startY: 0,
width: 0.4,
height: 0.4,
resizingPart: "bottomRightCorner",
},
draggingItem: {
offsetX: 0,
offsetY: 0,
startX: 0,
startY: 0,
x: -100,
y: -100,
},
})),
resize: assign((context, { dx, dy }) => {
if (context.selection.resizingPart === "bottomRightCorner") {
let newWidth = context.selection.startWidth - dx;
if (newWidth < MIN_SIZE) {
newWidth = MIN_SIZE;
}
if (context.selection.startX + newWidth > 1) {
newWidth = 1 - context.selection.startX;
}
let newHeight = context.selection.startHeight - dy;
if (newHeight < MIN_SIZE) {
newHeight = MIN_SIZE;
}
if (context.selection.startY + newHeight > 1) {
newHeight = 1 - context.selection.startY;
}
return {
...context,
selection: {
...context.selection,
width: newWidth,
height: newHeight,
},
};
} else if (context.selection.resizingPart === "topLeftCorner") {
let newWidth = context.selection.startWidth + dx;
let newHeight = context.selection.startHeight + dy;
let newX = context.selection.startX - dx;
let newY = context.selection.startY - dy;
if (newWidth < MIN_SIZE) {
newWidth = MIN_SIZE;
newX =
context.selection.startX +
context.selection.startWidth -
newWidth;
}
if (newHeight < MIN_SIZE) {
newHeight = MIN_SIZE;
newY =
context.selection.startY +
context.selection.startHeight -
newHeight;
}
if (newX < 0) {
newX = 0;
newWidth = context.selection.startX + context.selection.startWidth;
}
if (newY < 0) {
newY = 0;
newHeight =
context.selection.startY + context.selection.startHeight;
}
return {
...context,
selection: {
...context.selection,
x: newX,
y: newY,
width: newWidth,
height: newHeight,
},
};
} else if (context.selection.resizingPart === "outline") {
let newX = context.selection.startX - dx;
if (newX < 0) {
newX = 0;
}
if (newX + context.selection.width > 1) {
newX = 1 - context.selection.width;
}
let newY = context.selection.startY - dy;
if (newY < 0) {
newY = 0;
}
if (newY + context.selection.height > 1) {
newY = 1 - context.selection.height;
}
return {
...context,
selection: {
...context.selection,
x: newX,
y: newY,
},
};
} else if (context.selection.resizingPart === "topRightCorner") {
let newWidth = context.selection.startWidth - dx;
if (newWidth < MIN_SIZE) {
newWidth = MIN_SIZE;
}
if (context.selection.startX + newWidth > 1) {
newWidth = 1 - context.selection.startX;
}
let newY = context.selection.startY - dy;
let newHeight = context.selection.startHeight + dy;
if (newHeight < MIN_SIZE) {
newHeight = MIN_SIZE;
newY =
context.selection.startY +
context.selection.startHeight -
newHeight;
}
if (newY < 0) {
newY = 0;
newHeight =
context.selection.startY + context.selection.startHeight;
}
return {
...context,
selection: {
...context.selection,
width: newWidth,
height: newHeight,
y: newY,
},
};
} else if (context.selection.resizingPart === "bottomLeftCorner") {
let newWidth = context.selection.startWidth + dx;
let newHeight = context.selection.startHeight - dy;
let newX = context.selection.startX - dx;
if (newHeight < MIN_SIZE) {
newHeight = MIN_SIZE;
}
if (newWidth < MIN_SIZE) {
newWidth = MIN_SIZE;
newX =
context.selection.startX +
context.selection.startWidth -
newWidth;
}
if (newX < 0) {
newX = 0;
newWidth = context.selection.startX + context.selection.startWidth;
}
if (context.selection.startY + newHeight > 1) {
newHeight = 1 - context.selection.startY;
}
return {
...context,
selection: {
...context.selection,
x: newX,
width: newWidth,
height: newHeight,
},
};
} else if (context.selection.resizingPart === "bottomOutline") {
let newHeight = context.selection.startHeight - dy;
if (newHeight < MIN_SIZE) {
newHeight = MIN_SIZE;
}
if (context.selection.startY + newHeight > 1) {
newHeight = 1 - context.selection.startY;
}
return {
...context,
selection: {
...context.selection,
height: newHeight,
},
};
} else if (context.selection.resizingPart === "leftOutline") {
let newWidth = context.selection.startWidth + dx;
let newX = context.selection.startX - dx;
if (newWidth < MIN_SIZE) {
newWidth = MIN_SIZE;
newX =
context.selection.startX +
context.selection.startWidth -
newWidth;
}
if (newX < 0) {
newX = 0;
newWidth = context.selection.startX + context.selection.startWidth;
}
return {
...context,
selection: {
...context.selection,
x: newX,
width: newWidth,
},
};
} else if (context.selection.resizingPart === "rightOutline") {
let newWidth = context.selection.startWidth - dx;
if (newWidth < MIN_SIZE) {
newWidth = MIN_SIZE;
}
if (context.selection.startX + newWidth > 1) {
newWidth = 1 - context.selection.startX;
}
return {
...context,
selection: {
...context.selection,
width: newWidth,
},
};
} else if (context.selection.resizingPart === "topOutline") {
let newHeight = context.selection.startHeight + dy;
let newY = context.selection.startY - dy;
if (newHeight < MIN_SIZE) {
newHeight = MIN_SIZE;
newY =
context.selection.startY +
context.selection.startHeight -
newHeight;
}
if (newY < 0) {
newY = 0;
newHeight =
context.selection.startY + context.selection.startHeight;
}
return {
...context,
selection: {
...context.selection,
y: newY,
height: newHeight,
},
};
}
console.error(
`${context.selection.resizingPart} is unknown resizing part`
);
}),
updateSelectedApp: assign({
apps: (context) => {
const newApps = context.apps.map((app) => {
if (app.id !== context.selection.appId) return app;
const newApp = { ...app };
newApp.rect = {
x: context.selection.x,
y: context.selection.y,
width: context.selection.width,
height: context.selection.height,
};
return newApp;
});
return newApps;
},
}),
selectApp: assign({
selection: (context, event) => {
const selectedApp = getAppUnderMouse(context.apps, event.x, event.y);
const { rect } = selectedApp;
return {
...context.selection,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
appId: selectedApp.id,
};
},
}),
selectLatestApp: assign({
selection: (context) => {
const selectedApp = context.apps[context.apps.length - 1];
const { rect } = selectedApp;
return {
...context.selection,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
appId: selectedApp.id,
};
},
}),
addApp: assign({
apps: (context) => [
...context.apps,
{
id: uuidv4(),
template: context.draggingItem.app,
param: context.draggingItem.app.param,
rect: {
x: context.draggingItem.x,
y: context.draggingItem.y,
width: 0.2,
height: 0.2,
},
ref: spawn(AppMachine),
},
],
}),
deleteApp: assign({
apps: (context, event) => {
const app = getAppUnderMouse(context.apps, event.x, event.y);
return context.apps.filter((a) => a !== app);
},
}),
draggingMedia: assign((context, { dx, dy }) => {
let newX =
context.draggingItem.startX - dx - context.draggingItem.offsetX;
let newY =
context.draggingItem.startY - dy - context.draggingItem.offsetY;
return {
...context,
draggingItem: {
...context.draggingItem,
x: newX,
y: newY,
},
};
}),
displayAppOverlay: assign({
appOverlay: (context, event) => {
const { rect } = getAppUnderMouse(context.apps, event.x, event.y);
return {
...context.appOverlay,
...rect,
};
},
}),
updateAppOverlay: assign({
appOverlay: (context) => {
return {
...context.appOverlay,
x: context.selection.x,
y: context.selection.y,
width: context.selection.width,
height: context.selection.height,
};
},
}),
hideAppOverlay: assign({
appOverlay: { x: 0, y: 0, width: 0, height: 0 },
}),
updateSelectedAppSettings: assign({
apps: (context, event) => {
debugger;
const newApps = context.apps.map((app) => {
if (app.id !== context.selection.appId) return app;
const newApp = { ...app, param: event.param };
return newApp;
});
return newApps;
},
}),
},
}
);
function getAppUnderMouse(apps, x, y) {
return apps
.slice()
.reverse()
.find(
({ rect }) =>
x >= rect.x &&
y >= rect.y &&
x <= rect.x + rect.width &&
y <= rect.y + rect.height
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment