Skip to content

Commit 2b2a570

Browse files
add canvas support with panning & centering
1 parent 1e292be commit 2b2a570

File tree

3 files changed

+161
-70
lines changed

3 files changed

+161
-70
lines changed

editor/components/canvas/controller-zoom-control.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ export function ZoomControl({
77
scale,
88
stepper,
99
onChange,
10+
onReset,
1011
}: {
1112
onChange: (scale: number) => void;
1213
resetControl?: boolean;
1314
select?: boolean;
1415
stepper?: boolean;
1516
scale: number;
17+
onReset?: () => void;
1618
}) {
1719
const [isEditing, setIsEditing] = React.useState(false);
1820
const displayScale = (scale * 100).toFixed(0);
@@ -56,7 +58,9 @@ export function ZoomControl({
5658
</Valuedisplay>
5759
{scale !== 1 && (
5860
<RefreshSharpIcon
59-
onClick={() => onChange(1)}
61+
onClick={() => {
62+
onReset?.();
63+
}}
6064
style={{ color: "white", fontSize: 18 }}
6165
/>
6266
)}

editor/components/canvas/interactive-canvas.tsx

Lines changed: 89 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,107 @@
1-
import React, { useRef, useState } from "react";
1+
import React, { useEffect, useRef, useState } from "react";
22
import styled from "@emotion/styled";
3-
import { usePinch } from "@use-gesture/react";
3+
import { useGesture } from "@use-gesture/react";
44
import { Resizable } from "re-resizable";
55
import { ZoomControl } from "./controller-zoom-control";
66

7+
/**
8+
* A React Hook that returns a delta state.
9+
* When user completely stops interacting, after a short delay (600ms), set the value to false.
10+
* When user starts interacting, immidiately set the value to true.
11+
*
12+
* the condition rather if the user is currently interacting or not is set on higher level, which this function accepts the condition as a parameter.
13+
* @param interacting
14+
*/
15+
function useIsInteractingDelta(interacting: boolean) {
16+
throw new Error("Not implemented");
17+
}
18+
719
export function InteractiveCanvas({
820
children,
921
defaultSize,
1022
}: {
1123
defaultSize: { width: number; height: number };
1224
children?: React.ReactNode;
1325
}) {
14-
const [scale, setScale] = useState(1);
26+
const __canvas_width = 800;
27+
const __canvas_height = 900;
28+
const __margin = 20;
29+
const __y_start =
30+
defaultSize.height < __canvas_height - __margin * 2
31+
? (__canvas_height - defaultSize.height) / 2
32+
: __margin;
33+
const __initial_xy = [0, __y_start] as [number, number];
34+
const __initial_scale =
35+
defaultSize.width > __canvas_width
36+
? (__canvas_width - __margin * 2) / defaultSize.width
37+
: 1;
38+
39+
const [scale, setScale] = useState(__initial_scale);
40+
const [xy, setXY] = useState<[number, number]>(__initial_xy);
41+
42+
const [isPanning, setIsPanning] = useState(false);
43+
const [isZooming, setIsZooming] = useState(false);
44+
const isDeltaInteracting = isPanning || isZooming;
45+
46+
const ref = useRef();
47+
48+
useGesture(
49+
{
50+
onPinch: (state) => {
51+
setIsZooming(state.pinching);
52+
setScale(Math.max(scale + state.delta[0], 0.1));
53+
},
54+
onWheel: ({ delta: [x, y], wheeling }) => {
55+
setIsPanning(wheeling);
56+
setXY([xy[0] - x / scale, xy[1] - y / scale]);
57+
},
58+
},
59+
{ target: ref }
60+
);
1561

1662
return (
1763
<InteractiveCanvasWrapper id="interactive-canvas">
18-
<ScalableFrame onRescale={setScale} scale={scale}>
64+
<div
65+
id="event-listener"
66+
ref={ref}
67+
style={{
68+
flexGrow: 1,
69+
display: "flex",
70+
flexDirection: "column",
71+
alignItems: "center",
72+
}}
73+
>
1974
<Controls>
20-
<ZoomControl scale={scale} onChange={setScale} />
75+
<ZoomControl
76+
onReset={() => {
77+
setScale(__initial_scale);
78+
setXY(__initial_xy);
79+
}}
80+
scale={scale}
81+
onChange={setScale}
82+
/>
2183
</Controls>
22-
<ScalingAreaStaticRoot>
23-
<ScalingArea scale={scale}>
24-
<ResizableFrame defaultSize={defaultSize} scale={scale}>
25-
{children}
26-
</ResizableFrame>
27-
</ScalingArea>
28-
</ScalingAreaStaticRoot>
29-
</ScalableFrame>
84+
{/* <ScalingAreaStaticRoot> */}
85+
<TransformContainer
86+
scale={scale}
87+
xy={xy}
88+
isTransitioning={isDeltaInteracting}
89+
>
90+
<ResizableFrame defaultSize={defaultSize} scale={scale}>
91+
{children}
92+
</ResizableFrame>
93+
</TransformContainer>
94+
{/* </ScalingAreaStaticRoot> */}
95+
</div>
3096
</InteractiveCanvasWrapper>
3197
);
3298
}
3399

34100
const InteractiveCanvasWrapper = styled.div`
35101
display: flex;
36102
flex-direction: column;
37-
/* overflow-y: auto; */
38-
overflow-x: hidden;
39-
flex: 1;
103+
overflow: hidden;
104+
flex-grow: 1;
40105
`;
41106

42107
const Controls = styled.div`
@@ -46,67 +111,23 @@ const Controls = styled.div`
46111
justify-content: flex-end;
47112
`;
48113

49-
const ScalingAreaStaticRoot = styled.div`
50-
display: flex;
51-
align-items: flex-start; // when transform origin is top center.
52-
padding-top: 20px;
53-
justify-content: center;
54-
align-content: flex-start;
55-
align-self: stretch;
56-
flex: 1;
57-
max-height: 100vh; // TODO: make dynamic
58-
`;
59-
60-
function ScalableFrame({
61-
children,
62-
scale,
63-
onRescale,
64-
}: {
65-
scale: number;
66-
onRescale?: (scale: number) => void;
67-
children?: React.ReactNode;
68-
}) {
69-
const ref = useRef();
70-
71-
usePinch(
72-
(state) => {
73-
const prevscale = scale;
74-
const { offset } = state;
75-
const thisscale = offset[0];
76-
// const newscale = thisscale - prevscale;
77-
onRescale(thisscale);
78-
},
79-
{ target: ref }
80-
);
81-
82-
return (
83-
<div
84-
id="scale-event-listener"
85-
ref={ref}
86-
style={{
87-
display: "flex",
88-
flexDirection: "column",
89-
flex: 1,
90-
alignItems: "center",
91-
alignContent: "center",
92-
}}
93-
>
94-
{children}
95-
</div>
96-
);
97-
}
98-
99-
const ScalingArea = ({
114+
const TransformContainer = ({
100115
scale,
101116
children,
117+
xy,
118+
isTransitioning,
102119
}: {
103120
scale: number;
121+
xy: [number, number];
122+
isTransitioning: boolean;
104123
children: React.ReactNode;
105124
}) => {
106125
return (
107126
<div
108127
style={{
109-
transform: `scale(${scale})`,
128+
pointerEvents: isTransitioning ? "none" : undefined,
129+
transform: `scale(${scale}) translateX(${xy[0]}px) translateY(${xy[1]}px)`,
130+
willChange: "transform",
110131
transformOrigin: "top center",
111132
}}
112133
>

editor/pages/_app.tsx

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import React from "react";
1+
import React, { useEffect } from "react";
22
import { Global, css } from "@emotion/react";
33
import Head from "next/head";
44
import { EditorThemeProvider } from "@editor-ui/theme";
55
import { colors } from "theme";
6+
import { useRouter } from "next/router";
67

78
function GlobalCss() {
89
return (
@@ -17,6 +18,10 @@ function GlobalCss() {
1718
margin: 0px;
1819
padding: 0;
1920
font-family: "Helvetica Nueue", "Roboto", sans-serif;
21+
22+
/* for editor canvas */
23+
overscroll-behavior-x: none;
24+
overscroll-behavior-y: none;
2025
}
2126
2227
iframe {
@@ -106,6 +111,38 @@ function SeoMeta() {
106111
}
107112

108113
function EditorApp({ Component, pageProps }) {
114+
const router = useRouter();
115+
const _path = router.asPath.replace("/", "");
116+
const analyzed = analyze_dynamic_input(_path);
117+
useEffect(() => {
118+
if (pageProps.statusCode == 404 && analyzed) {
119+
switch (analyzed.ns) {
120+
case "figma": {
121+
const { file, node } = parseFileAndNodeId(_path) ?? {};
122+
switch (analyzed.result) {
123+
case "node": {
124+
router.replace("/files/[key]/[node]", `/files/${file}/${node}`);
125+
break;
126+
}
127+
case "file": {
128+
router.replace("/files/[key]", `/files/${file}`);
129+
break;
130+
}
131+
case "empty":
132+
default: {
133+
break;
134+
}
135+
}
136+
break;
137+
}
138+
case "unknown":
139+
default: {
140+
break;
141+
}
142+
}
143+
}
144+
}, [analyzed]);
145+
109146
return (
110147
<React.Fragment>
111148
<HeadInjection />
@@ -117,3 +154,32 @@ function EditorApp({ Component, pageProps }) {
117154
}
118155

119156
export default EditorApp;
157+
158+
import {
159+
analyze as figmaurlAnalize,
160+
parseFileAndNodeId,
161+
} from "@design-sdk/figma-url";
162+
163+
function analyze_dynamic_input(input: string) {
164+
const _isurl = isurl(input);
165+
if (_isurl) {
166+
return {
167+
ns: "figma",
168+
result: figmaurlAnalize(input),
169+
};
170+
}
171+
172+
return {
173+
ns: "unknown",
174+
result: input,
175+
};
176+
}
177+
178+
const isurl = (s: string) => {
179+
try {
180+
new URL(s);
181+
return true;
182+
} catch (e) {
183+
return false;
184+
}
185+
};

0 commit comments

Comments
 (0)