@pictor-sdk/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/index.d.mts +833 -0
- package/dist/index.d.ts +833 -0
- package/dist/index.js +4620 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4643 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +68 -0
- package/src/Editor.tsx +276 -0
- package/src/ToolPanelHost.tsx +98 -0
- package/src/components/Slider.tsx +172 -0
- package/src/components/StraightenRuler.tsx +57 -0
- package/src/components/ToolRail.tsx +58 -0
- package/src/components/TopBar.tsx +53 -0
- package/src/composer/TextComposer.tsx +540 -0
- package/src/composer/keyboardLayout.ts +130 -0
- package/src/composer/useTextSession.ts +230 -0
- package/src/gestures/CropGestureSurface.tsx +270 -0
- package/src/gestures/CropGrid.tsx +24 -0
- package/src/gestures/DrawingSurface.tsx +77 -0
- package/src/gestures/EditorGestures.tsx +97 -0
- package/src/gestures/InteractionGuides.tsx +161 -0
- package/src/gestures/LayerGestureSurface.tsx +462 -0
- package/src/gestures/StoryMediaGestureSurface.tsx +517 -0
- package/src/gestures/TiltShiftGestureSurface.tsx +189 -0
- package/src/gestures/controller.ts +152 -0
- package/src/gestures/drawController.ts +104 -0
- package/src/gestures/interactionState.tsx +81 -0
- package/src/gestures/layerGestureWorklet.ts +198 -0
- package/src/gestures/mediaGestureWorklet.ts +159 -0
- package/src/gestures/routing.ts +42 -0
- package/src/gestures/settleCoordinator.ts +61 -0
- package/src/gestures/transformOwnership.ts +91 -0
- package/src/hostCallbacks.tsx +62 -0
- package/src/index.ts +72 -0
- package/src/panels/AddPhotoPanel.tsx +101 -0
- package/src/panels/AdjustPanel.tsx +158 -0
- package/src/panels/ColorPanel.tsx +193 -0
- package/src/panels/CropPanel.tsx +185 -0
- package/src/panels/DrawPanel.tsx +106 -0
- package/src/panels/FilterStrip.tsx +153 -0
- package/src/panels/LayersPanel.tsx +85 -0
- package/src/panels/LayoutPanel.tsx +55 -0
- package/src/panels/LuxPanel.tsx +122 -0
- package/src/panels/MediaPanel.tsx +73 -0
- package/src/panels/PerspectivePanel.tsx +157 -0
- package/src/panels/StickerPanel.tsx +78 -0
- package/src/panels/TextPanel.tsx +177 -0
- package/src/panels/TiltShiftPanel.tsx +146 -0
- package/src/panels/addPhotoController.ts +196 -0
- package/src/panels/cropMath.ts +37 -0
- package/src/panels/layoutMath.ts +58 -0
- package/src/panels/useTargetImageLayer.ts +41 -0
- package/src/theme.ts +61 -0
- package/src/uiState.tsx +57 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,4643 @@
|
|
|
1
|
+
// src/Editor.tsx
|
|
2
|
+
import {
|
|
3
|
+
EditorProvider,
|
|
4
|
+
selectionChromeEnabled,
|
|
5
|
+
useEditor as useEditor25
|
|
6
|
+
} from "@pictor-sdk/core";
|
|
7
|
+
import { EditorCanvas } from "@pictor-sdk/skia";
|
|
8
|
+
import React19, { useState as useState11 } from "react";
|
|
9
|
+
import { View as View21 } from "react-native";
|
|
10
|
+
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
11
|
+
|
|
12
|
+
// src/hostCallbacks.tsx
|
|
13
|
+
import { createContext, useContext, useRef } from "react";
|
|
14
|
+
import { jsx } from "react/jsx-runtime";
|
|
15
|
+
var Context = createContext(null);
|
|
16
|
+
function HostCallbacksProvider({
|
|
17
|
+
value,
|
|
18
|
+
children
|
|
19
|
+
}) {
|
|
20
|
+
const ref = useRef(value);
|
|
21
|
+
ref.current = value;
|
|
22
|
+
return /* @__PURE__ */ jsx(Context.Provider, { value: ref, children });
|
|
23
|
+
}
|
|
24
|
+
function useHostCallbacks() {
|
|
25
|
+
const fallback = useRef({});
|
|
26
|
+
return useContext(Context) ?? fallback;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/gestures/DrawingSurface.tsx
|
|
30
|
+
import { useEditor, useEditorStore } from "@pictor-sdk/core";
|
|
31
|
+
import { samplePixelColor, useCanvasMetrics, useRenderer } from "@pictor-sdk/skia";
|
|
32
|
+
import { useMemo, useRef as useRef3 } from "react";
|
|
33
|
+
import { StyleSheet } from "react-native";
|
|
34
|
+
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
35
|
+
import Animated from "react-native-reanimated";
|
|
36
|
+
import { runOnJS } from "react-native-worklets";
|
|
37
|
+
|
|
38
|
+
// src/uiState.tsx
|
|
39
|
+
import { createContext as createContext2, useCallback, useContext as useContext2, useRef as useRef2, useSyncExternalStore } from "react";
|
|
40
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
41
|
+
function createUiStateStore() {
|
|
42
|
+
const values = /* @__PURE__ */ new Map();
|
|
43
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
44
|
+
return {
|
|
45
|
+
get: (key, fallback) => values.has(key) ? values.get(key) : fallback,
|
|
46
|
+
set(key, value) {
|
|
47
|
+
values.set(key, value);
|
|
48
|
+
for (const l of [...listeners]) l();
|
|
49
|
+
},
|
|
50
|
+
subscribe(listener) {
|
|
51
|
+
listeners.add(listener);
|
|
52
|
+
return () => listeners.delete(listener);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
var UiStateContext = createContext2(null);
|
|
57
|
+
function UiStateProvider({ children }) {
|
|
58
|
+
const ref = useRef2(null);
|
|
59
|
+
if (!ref.current) ref.current = createUiStateStore();
|
|
60
|
+
return /* @__PURE__ */ jsx2(UiStateContext.Provider, { value: ref.current, children });
|
|
61
|
+
}
|
|
62
|
+
function useUiState(key, fallback) {
|
|
63
|
+
const store = useContext2(UiStateContext);
|
|
64
|
+
if (!store) throw new Error("useUiState must be used inside <Editor> (UiStateProvider)");
|
|
65
|
+
const value = useSyncExternalStore(
|
|
66
|
+
store.subscribe,
|
|
67
|
+
() => store.get(key, fallback),
|
|
68
|
+
() => store.get(key, fallback)
|
|
69
|
+
);
|
|
70
|
+
const set = useCallback((v) => store.set(key, v), [store, key]);
|
|
71
|
+
return [value, set];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/gestures/drawController.ts
|
|
75
|
+
import { createId } from "@pictor-sdk/core";
|
|
76
|
+
function createDrawController(store, getMetrics, getBrush) {
|
|
77
|
+
let drawing = false;
|
|
78
|
+
let layerId = null;
|
|
79
|
+
let points = [];
|
|
80
|
+
let baseStrokes = [];
|
|
81
|
+
function toDocPoint(xDp, yDp) {
|
|
82
|
+
const metrics = getMetrics();
|
|
83
|
+
if (!metrics || metrics.width === 0 || metrics.height === 0) return null;
|
|
84
|
+
return { x: xDp / metrics.width, y: yDp / metrics.height };
|
|
85
|
+
}
|
|
86
|
+
function ensureDrawLayer() {
|
|
87
|
+
const doc = store.getState().document;
|
|
88
|
+
for (let i = doc.layers.length - 1; i >= 0; i--) {
|
|
89
|
+
const layer = doc.layers[i];
|
|
90
|
+
if (layer.type === "draw" && !layer.locked && !layer.hidden) return layer.id;
|
|
91
|
+
}
|
|
92
|
+
const id = createId("draw");
|
|
93
|
+
store.dispatch({
|
|
94
|
+
type: "layer/add",
|
|
95
|
+
layer: {
|
|
96
|
+
id,
|
|
97
|
+
type: "draw",
|
|
98
|
+
transform: { x: 0.5, y: 0.5, scale: 1, rotation: 0 },
|
|
99
|
+
opacity: 1,
|
|
100
|
+
blendMode: "normal",
|
|
101
|
+
data: { strokes: [] }
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return id;
|
|
105
|
+
}
|
|
106
|
+
function currentStroke() {
|
|
107
|
+
const brush = getBrush();
|
|
108
|
+
return { points, color: brush.color, width: brush.width, brush: brush.brush };
|
|
109
|
+
}
|
|
110
|
+
function patch() {
|
|
111
|
+
if (!layerId) return;
|
|
112
|
+
store.dispatch({
|
|
113
|
+
type: "layer/patchData",
|
|
114
|
+
id: layerId,
|
|
115
|
+
patch: { strokes: [...baseStrokes, currentStroke()] }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
begin(xDp, yDp) {
|
|
120
|
+
const point = toDocPoint(xDp, yDp);
|
|
121
|
+
if (!point || drawing) return;
|
|
122
|
+
drawing = true;
|
|
123
|
+
store.dispatch({ type: "history/beginBatch", label: "draw" });
|
|
124
|
+
layerId = ensureDrawLayer();
|
|
125
|
+
const layer = store.getState().document.layers.find((l) => l.id === layerId);
|
|
126
|
+
baseStrokes = layer?.data?.strokes ?? [];
|
|
127
|
+
points = [point];
|
|
128
|
+
patch();
|
|
129
|
+
},
|
|
130
|
+
move(xDp, yDp) {
|
|
131
|
+
if (!drawing) return;
|
|
132
|
+
const point = toDocPoint(xDp, yDp);
|
|
133
|
+
if (!point) return;
|
|
134
|
+
const last = points[points.length - 1];
|
|
135
|
+
if (last && Math.hypot(point.x - last.x, point.y - last.y) < 3e-3) return;
|
|
136
|
+
points = [...points, point];
|
|
137
|
+
patch();
|
|
138
|
+
},
|
|
139
|
+
end() {
|
|
140
|
+
if (!drawing) return;
|
|
141
|
+
drawing = false;
|
|
142
|
+
store.dispatch({ type: "history/endBatch" });
|
|
143
|
+
layerId = null;
|
|
144
|
+
points = [];
|
|
145
|
+
baseStrokes = [];
|
|
146
|
+
},
|
|
147
|
+
isDrawing: () => drawing
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/gestures/DrawingSurface.tsx
|
|
152
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
153
|
+
var DEFAULT_BRUSH = { brush: "pen", color: "#ffffff", width: 0.012 };
|
|
154
|
+
function DrawingSurface() {
|
|
155
|
+
const store = useEditorStore();
|
|
156
|
+
const { document } = useEditor();
|
|
157
|
+
const renderer = useRenderer();
|
|
158
|
+
const metrics = useCanvasMetrics();
|
|
159
|
+
const metricsRef = useRef3(metrics);
|
|
160
|
+
metricsRef.current = metrics;
|
|
161
|
+
const [brush, setBrush] = useUiState("draw.brush", DEFAULT_BRUSH);
|
|
162
|
+
const [eyedropper, setEyedropper] = useUiState("draw.eyedropper", false);
|
|
163
|
+
const brushRef = useRef3(brush);
|
|
164
|
+
brushRef.current = brush;
|
|
165
|
+
const stateRef = useRef3({ eyedropper, document, setBrush, setEyedropper });
|
|
166
|
+
stateRef.current = { eyedropper, document, setBrush, setEyedropper };
|
|
167
|
+
const controller = useMemo(
|
|
168
|
+
() => createDrawController(store, () => metricsRef.current, () => brushRef.current),
|
|
169
|
+
[store]
|
|
170
|
+
);
|
|
171
|
+
const gesture = useMemo(() => {
|
|
172
|
+
const sample = async (x, y) => {
|
|
173
|
+
const m = metricsRef.current;
|
|
174
|
+
const s = stateRef.current;
|
|
175
|
+
if (!m || !s.eyedropper) return;
|
|
176
|
+
const color = await samplePixelColor(renderer, s.document, {
|
|
177
|
+
x: x / m.width,
|
|
178
|
+
y: y / m.height
|
|
179
|
+
});
|
|
180
|
+
if (color) s.setBrush({ ...brushRef.current, color });
|
|
181
|
+
s.setEyedropper(false);
|
|
182
|
+
};
|
|
183
|
+
const beginOrSample = (x, y) => {
|
|
184
|
+
if (stateRef.current.eyedropper) {
|
|
185
|
+
void sample(x, y);
|
|
186
|
+
} else {
|
|
187
|
+
controller.begin(x, y);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
return Gesture.Pan().minDistance(0).onStart((e) => {
|
|
191
|
+
"worklet";
|
|
192
|
+
runOnJS(beginOrSample)(e.x, e.y);
|
|
193
|
+
}).onUpdate((e) => {
|
|
194
|
+
"worklet";
|
|
195
|
+
runOnJS(controller.move)(e.x, e.y);
|
|
196
|
+
}).onFinalize(() => {
|
|
197
|
+
"worklet";
|
|
198
|
+
runOnJS(controller.end)();
|
|
199
|
+
});
|
|
200
|
+
}, [controller, renderer]);
|
|
201
|
+
return /* @__PURE__ */ jsx3(GestureDetector, { gesture, children: /* @__PURE__ */ jsx3(Animated.View, { style: StyleSheet.absoluteFill }) });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/gestures/CropGestureSurface.tsx
|
|
205
|
+
import {
|
|
206
|
+
clampCropTransform,
|
|
207
|
+
cropFitScale,
|
|
208
|
+
cropPanBounds,
|
|
209
|
+
focalZoomTransform
|
|
210
|
+
} from "@pictor-sdk/core";
|
|
211
|
+
import { useCanvasMetrics as useCanvasMetrics2 } from "@pictor-sdk/skia";
|
|
212
|
+
import { useEffect, useMemo as useMemo3, useRef as useRef5 } from "react";
|
|
213
|
+
import { StyleSheet as StyleSheet2 } from "react-native";
|
|
214
|
+
import { Gesture as Gesture2, GestureDetector as GestureDetector2 } from "react-native-gesture-handler";
|
|
215
|
+
import Animated2, { useDerivedValue, useSharedValue, withTiming } from "react-native-reanimated";
|
|
216
|
+
import { runOnJS as runOnJS2 } from "react-native-worklets";
|
|
217
|
+
|
|
218
|
+
// src/panels/useTargetImageLayer.ts
|
|
219
|
+
import {
|
|
220
|
+
pinnedTargetImageLayer,
|
|
221
|
+
resolveTargetImageLayer,
|
|
222
|
+
useEditor as useEditor2
|
|
223
|
+
} from "@pictor-sdk/core";
|
|
224
|
+
import { useRef as useRef4 } from "react";
|
|
225
|
+
function useTargetImageLayer() {
|
|
226
|
+
const { document, selection } = useEditor2();
|
|
227
|
+
const pinned = useRef4(null);
|
|
228
|
+
if (pinned.current === null) {
|
|
229
|
+
pinned.current = resolveTargetImageLayer(document, selection)?.id ?? null;
|
|
230
|
+
}
|
|
231
|
+
return pinnedTargetImageLayer(document, pinned.current) ?? null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/gestures/CropGestureSurface.tsx
|
|
235
|
+
import { useEditor as useEditor3 } from "@pictor-sdk/core";
|
|
236
|
+
|
|
237
|
+
// src/gestures/interactionState.tsx
|
|
238
|
+
import { NO_SNAP } from "@pictor-sdk/core";
|
|
239
|
+
import { createContext as createContext3, useCallback as useCallback2, useContext as useContext3, useMemo as useMemo2, useState } from "react";
|
|
240
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
241
|
+
var Context2 = createContext3(null);
|
|
242
|
+
function InteractionProvider({ children }) {
|
|
243
|
+
const [preview, setPreviewValue] = useState(null);
|
|
244
|
+
const [cropActive, setCropActive] = useState(false);
|
|
245
|
+
const [layerDragActive, setLayerDragActive] = useState(false);
|
|
246
|
+
const [overDeleteTarget, setOverDeleteTarget] = useState(false);
|
|
247
|
+
const [snap, setSnapValue] = useState(NO_SNAP);
|
|
248
|
+
const setPreview = useCallback2((value2) => setPreviewValue(value2), []);
|
|
249
|
+
const setSnap = useCallback2((value2) => {
|
|
250
|
+
setSnapValue(
|
|
251
|
+
(current) => current.centerX === value2.centerX && current.centerY === value2.centerY && current.rotation === value2.rotation ? current : value2
|
|
252
|
+
);
|
|
253
|
+
}, []);
|
|
254
|
+
const value = useMemo2(
|
|
255
|
+
() => ({
|
|
256
|
+
previews: preview ? [preview] : [],
|
|
257
|
+
setPreview,
|
|
258
|
+
cropActive,
|
|
259
|
+
setCropActive,
|
|
260
|
+
layerDragActive,
|
|
261
|
+
setLayerDragActive,
|
|
262
|
+
overDeleteTarget,
|
|
263
|
+
setOverDeleteTarget,
|
|
264
|
+
snap,
|
|
265
|
+
setSnap
|
|
266
|
+
}),
|
|
267
|
+
[preview, setPreview, cropActive, layerDragActive, overDeleteTarget, snap, setSnap]
|
|
268
|
+
);
|
|
269
|
+
return /* @__PURE__ */ jsx4(Context2.Provider, { value, children });
|
|
270
|
+
}
|
|
271
|
+
function useInteractionState() {
|
|
272
|
+
const value = useContext3(Context2);
|
|
273
|
+
if (!value) throw new Error("interaction components must be used inside <Editor>");
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/gestures/CropGestureSurface.tsx
|
|
278
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
279
|
+
function CropGestureSurface({ fit, maxScale, rotationEnabled, onInteractionChange }) {
|
|
280
|
+
const { document, dispatch, profile } = useEditor3();
|
|
281
|
+
const layer = useTargetImageLayer();
|
|
282
|
+
const metrics = useCanvasMetrics2();
|
|
283
|
+
const interaction = useInteractionState();
|
|
284
|
+
const config = profile.interaction?.crop;
|
|
285
|
+
const source = layer?.data?.intrinsic;
|
|
286
|
+
const resolvedFit = fit ?? config?.fit ?? "contain";
|
|
287
|
+
const resolvedMax = maxScale ?? config?.maxScale ?? 8;
|
|
288
|
+
const resolvedRotation = rotationEnabled ?? config?.rotationEnabled ?? false;
|
|
289
|
+
const liveX = useSharedValue(layer?.transform.x ?? 0.5);
|
|
290
|
+
const liveY = useSharedValue(layer?.transform.y ?? 0.5);
|
|
291
|
+
const liveScale = useSharedValue(layer?.transform.scale ?? 1);
|
|
292
|
+
const baseX = useSharedValue(layer?.transform.x ?? 0.5);
|
|
293
|
+
const baseY = useSharedValue(layer?.transform.y ?? 0.5);
|
|
294
|
+
const baseScale = useSharedValue(layer?.transform.scale ?? 1);
|
|
295
|
+
const liveRotation = useSharedValue(layer?.transform.rotation ?? 0);
|
|
296
|
+
const baseRotation = useSharedValue(layer?.transform.rotation ?? 0);
|
|
297
|
+
const panX = useSharedValue(0);
|
|
298
|
+
const panY = useSharedValue(0);
|
|
299
|
+
const pinchScale = useSharedValue(1);
|
|
300
|
+
const rotateDelta = useSharedValue(0);
|
|
301
|
+
const focalX = useSharedValue(0.5);
|
|
302
|
+
const focalY = useSharedValue(0.5);
|
|
303
|
+
const activeRecognizers = useSharedValue(0);
|
|
304
|
+
const commitTimer = useRef5(null);
|
|
305
|
+
const minScale = source ? cropFitScale(source, document.size, resolvedFit, layer?.transform.rotation ?? 0) : 1;
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
if (!layer) return;
|
|
308
|
+
liveX.value = layer.transform.x;
|
|
309
|
+
liveY.value = layer.transform.y;
|
|
310
|
+
liveScale.value = layer.transform.scale;
|
|
311
|
+
liveRotation.value = layer.transform.rotation;
|
|
312
|
+
}, [layer?.transform.x, layer?.transform.y, layer?.transform.scale, layer?.transform.rotation]);
|
|
313
|
+
const previewTransform = useDerivedValue(() => {
|
|
314
|
+
const ratio = liveScale.value / Math.max(baseScale.value, 1e-6);
|
|
315
|
+
return [
|
|
316
|
+
{ translateX: liveX.value * document.size.width },
|
|
317
|
+
{ translateY: liveY.value * document.size.height },
|
|
318
|
+
{ rotate: liveRotation.value - baseRotation.value },
|
|
319
|
+
{ scale: ratio },
|
|
320
|
+
{ translateX: -baseX.value * document.size.width },
|
|
321
|
+
{ translateY: -baseY.value * document.size.height }
|
|
322
|
+
];
|
|
323
|
+
}, [document.size.width, document.size.height]);
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
if (!layer) return;
|
|
326
|
+
interaction.setPreview({ layerId: layer.id, transform: previewTransform });
|
|
327
|
+
return () => interaction.setPreview(null);
|
|
328
|
+
}, [layer?.id, previewTransform, interaction.setPreview]);
|
|
329
|
+
const setActive = (active) => {
|
|
330
|
+
interaction.setCropActive(active);
|
|
331
|
+
onInteractionChange?.(active);
|
|
332
|
+
};
|
|
333
|
+
const commit = (candidate) => {
|
|
334
|
+
if (!layer || !source) return;
|
|
335
|
+
if (commitTimer.current) clearTimeout(commitTimer.current);
|
|
336
|
+
const settledMinScale = cropFitScale(source, document.size, resolvedFit, candidate.rotation);
|
|
337
|
+
const bounds = cropPanBounds(source, document.size, candidate.scale, settledMinScale, resolvedMax, candidate.rotation);
|
|
338
|
+
const settled = clampCropTransform(candidate, bounds);
|
|
339
|
+
liveX.value = withTiming(settled.x, { duration: 160 });
|
|
340
|
+
liveY.value = withTiming(settled.y, { duration: 160 });
|
|
341
|
+
liveScale.value = withTiming(settled.scale, { duration: 160 });
|
|
342
|
+
liveRotation.value = withTiming(settled.rotation, { duration: 160 });
|
|
343
|
+
commitTimer.current = setTimeout(() => {
|
|
344
|
+
dispatch({
|
|
345
|
+
type: "layer/transform",
|
|
346
|
+
id: layer.id,
|
|
347
|
+
patch: {
|
|
348
|
+
x: settled.x,
|
|
349
|
+
y: settled.y,
|
|
350
|
+
scale: settled.scale,
|
|
351
|
+
...resolvedRotation ? { rotation: settled.rotation } : {}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
baseX.value = settled.x;
|
|
355
|
+
baseY.value = settled.y;
|
|
356
|
+
baseScale.value = settled.scale;
|
|
357
|
+
baseRotation.value = settled.rotation;
|
|
358
|
+
liveX.value = settled.x;
|
|
359
|
+
liveY.value = settled.y;
|
|
360
|
+
liveScale.value = settled.scale;
|
|
361
|
+
liveRotation.value = settled.rotation;
|
|
362
|
+
setActive(false);
|
|
363
|
+
}, 165);
|
|
364
|
+
};
|
|
365
|
+
const toggleZoom = (xDp, yDp) => {
|
|
366
|
+
if (!layer || !source || !metrics) return;
|
|
367
|
+
baseX.value = layer.transform.x;
|
|
368
|
+
baseY.value = layer.transform.y;
|
|
369
|
+
baseScale.value = layer.transform.scale;
|
|
370
|
+
baseRotation.value = layer.transform.rotation;
|
|
371
|
+
liveX.value = layer.transform.x;
|
|
372
|
+
liveY.value = layer.transform.y;
|
|
373
|
+
liveScale.value = layer.transform.scale;
|
|
374
|
+
liveRotation.value = layer.transform.rotation;
|
|
375
|
+
setActive(true);
|
|
376
|
+
const atMinimum = Math.abs(layer.transform.scale - minScale) < 0.01;
|
|
377
|
+
const targetScale = atMinimum ? Math.min(resolvedMax, minScale * (config?.doubleTapScale ?? 2)) : minScale;
|
|
378
|
+
commit(focalZoomTransform(layer.transform, targetScale, {
|
|
379
|
+
x: xDp / metrics.width,
|
|
380
|
+
y: yDp / metrics.height
|
|
381
|
+
}));
|
|
382
|
+
};
|
|
383
|
+
const gesture = useMemo3(() => {
|
|
384
|
+
if (!metrics || !source || !layer) return Gesture2.Tap();
|
|
385
|
+
const begin = () => {
|
|
386
|
+
"worklet";
|
|
387
|
+
if (activeRecognizers.value === 0) {
|
|
388
|
+
baseX.value = liveX.value;
|
|
389
|
+
baseY.value = liveY.value;
|
|
390
|
+
baseScale.value = liveScale.value;
|
|
391
|
+
baseRotation.value = liveRotation.value;
|
|
392
|
+
panX.value = 0;
|
|
393
|
+
panY.value = 0;
|
|
394
|
+
pinchScale.value = 1;
|
|
395
|
+
rotateDelta.value = 0;
|
|
396
|
+
runOnJS2(setActive)(true);
|
|
397
|
+
}
|
|
398
|
+
activeRecognizers.value += 1;
|
|
399
|
+
};
|
|
400
|
+
const updateLive = () => {
|
|
401
|
+
"worklet";
|
|
402
|
+
const rotation = baseRotation.value + rotateDelta.value;
|
|
403
|
+
const naturalWidth = document.size.width;
|
|
404
|
+
const naturalHeight = document.size.width * (source.height / source.width);
|
|
405
|
+
const cos = Math.abs(Math.cos(rotation));
|
|
406
|
+
const sin = Math.abs(Math.sin(rotation));
|
|
407
|
+
const rotatedWidth = naturalWidth * cos + naturalHeight * sin;
|
|
408
|
+
const rotatedHeight = naturalWidth * sin + naturalHeight * cos;
|
|
409
|
+
const widthScale = document.size.width / rotatedWidth;
|
|
410
|
+
const heightScale = document.size.height / rotatedHeight;
|
|
411
|
+
const dynamicMin = resolvedFit === "contain" ? Math.min(widthScale, heightScale) : Math.max(widthScale, heightScale);
|
|
412
|
+
const rawScale = baseScale.value * pinchScale.value;
|
|
413
|
+
const resistance = config?.resistance ?? 0.35;
|
|
414
|
+
const scale = rawScale < dynamicMin ? dynamicMin - (dynamicMin - rawScale) * resistance : rawScale > resolvedMax ? resolvedMax + (rawScale - resolvedMax) * resistance : rawScale;
|
|
415
|
+
const ratio = scale / Math.max(baseScale.value, 1e-6);
|
|
416
|
+
const zoomX = focalX.value - (focalX.value - baseX.value) * ratio;
|
|
417
|
+
const zoomY = focalY.value - (focalY.value - baseY.value) * ratio;
|
|
418
|
+
liveRotation.value = rotation;
|
|
419
|
+
const dx = Math.max(0, (rotatedWidth * scale - document.size.width) / (2 * document.size.width));
|
|
420
|
+
const dy = Math.max(0, (rotatedHeight * scale - document.size.height) / (2 * document.size.height));
|
|
421
|
+
liveScale.value = scale;
|
|
422
|
+
const rawX = zoomX + panX.value;
|
|
423
|
+
const rawY = zoomY + panY.value;
|
|
424
|
+
const minX = 0.5 - dx;
|
|
425
|
+
const maxX = 0.5 + dx;
|
|
426
|
+
const minY = 0.5 - dy;
|
|
427
|
+
const maxY = 0.5 + dy;
|
|
428
|
+
liveX.value = rawX < minX ? minX - (minX - rawX) * resistance : rawX > maxX ? maxX + (rawX - maxX) * resistance : rawX;
|
|
429
|
+
liveY.value = rawY < minY ? minY - (minY - rawY) * resistance : rawY > maxY ? maxY + (rawY - maxY) * resistance : rawY;
|
|
430
|
+
};
|
|
431
|
+
const finish = () => {
|
|
432
|
+
"worklet";
|
|
433
|
+
activeRecognizers.value = Math.max(0, activeRecognizers.value - 1);
|
|
434
|
+
if (activeRecognizers.value === 0) {
|
|
435
|
+
runOnJS2(commit)({
|
|
436
|
+
x: liveX.value,
|
|
437
|
+
y: liveY.value,
|
|
438
|
+
scale: liveScale.value,
|
|
439
|
+
rotation: liveRotation.value,
|
|
440
|
+
...layer.transform.anchor ? { anchor: layer.transform.anchor } : {},
|
|
441
|
+
...layer.transform.flipX !== void 0 ? { flipX: layer.transform.flipX } : {},
|
|
442
|
+
...layer.transform.flipY !== void 0 ? { flipY: layer.transform.flipY } : {}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
const pan = Gesture2.Pan().minPointers(1).onStart(begin).onUpdate((event) => {
|
|
447
|
+
"worklet";
|
|
448
|
+
panX.value = event.translationX / metrics.width;
|
|
449
|
+
panY.value = event.translationY / metrics.height;
|
|
450
|
+
updateLive();
|
|
451
|
+
}).onFinalize(finish);
|
|
452
|
+
const pinch = Gesture2.Pinch().onStart(begin).onUpdate((event) => {
|
|
453
|
+
"worklet";
|
|
454
|
+
pinchScale.value = event.scale;
|
|
455
|
+
focalX.value = event.focalX / metrics.width;
|
|
456
|
+
focalY.value = event.focalY / metrics.height;
|
|
457
|
+
updateLive();
|
|
458
|
+
}).onFinalize(finish);
|
|
459
|
+
const rotate = Gesture2.Rotation().enabled(resolvedRotation).onStart(begin).onUpdate((event) => {
|
|
460
|
+
"worklet";
|
|
461
|
+
rotateDelta.value = event.rotation;
|
|
462
|
+
updateLive();
|
|
463
|
+
}).onFinalize(finish);
|
|
464
|
+
const doubleTap = Gesture2.Tap().numberOfTaps(2).onEnd((event) => {
|
|
465
|
+
"worklet";
|
|
466
|
+
runOnJS2(toggleZoom)(event.x, event.y);
|
|
467
|
+
});
|
|
468
|
+
return Gesture2.Exclusive(doubleTap, Gesture2.Simultaneous(pan, pinch, rotate));
|
|
469
|
+
}, [metrics, source, layer, resolvedFit, resolvedMax, resolvedRotation, minScale, document.size.width, document.size.height, config?.resistance, config?.doubleTapScale]);
|
|
470
|
+
if (!layer) return null;
|
|
471
|
+
return /* @__PURE__ */ jsx5(GestureDetector2, { gesture, children: /* @__PURE__ */ jsx5(Animated2.View, { style: StyleSheet2.absoluteFill }) });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/gestures/TiltShiftGestureSurface.tsx
|
|
475
|
+
import {
|
|
476
|
+
moveTiltCenter,
|
|
477
|
+
needsTiltShift,
|
|
478
|
+
rotateTiltBand,
|
|
479
|
+
scaleTiltRadius,
|
|
480
|
+
tiltShiftPatch,
|
|
481
|
+
useEditor as useEditor4
|
|
482
|
+
} from "@pictor-sdk/core";
|
|
483
|
+
import { useCanvasMetrics as useCanvasMetrics3 } from "@pictor-sdk/skia";
|
|
484
|
+
import { useMemo as useMemo4, useRef as useRef6 } from "react";
|
|
485
|
+
import { StyleSheet as StyleSheet3 } from "react-native";
|
|
486
|
+
import { Gesture as Gesture3, GestureDetector as GestureDetector3 } from "react-native-gesture-handler";
|
|
487
|
+
import Animated3, { useAnimatedStyle, useSharedValue as useSharedValue2 } from "react-native-reanimated";
|
|
488
|
+
import { runOnJS as runOnJS3 } from "react-native-worklets";
|
|
489
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
490
|
+
function TiltShiftGestureSurface() {
|
|
491
|
+
const { dispatch } = useEditor4();
|
|
492
|
+
const layer = useTargetImageLayer();
|
|
493
|
+
const metrics = useCanvasMetrics3();
|
|
494
|
+
const tilt = layer?.data?.tiltShift;
|
|
495
|
+
const active = needsTiltShift(tilt);
|
|
496
|
+
const origin = useRef6(null);
|
|
497
|
+
const live = useRef6(null);
|
|
498
|
+
live.current = active ? tilt : null;
|
|
499
|
+
const width = metrics?.width ?? 0;
|
|
500
|
+
const height = metrics?.height ?? 0;
|
|
501
|
+
const oX = useSharedValue2(0);
|
|
502
|
+
const oY = useSharedValue2(0);
|
|
503
|
+
const oR = useSharedValue2(0);
|
|
504
|
+
const oRot = useSharedValue2(0);
|
|
505
|
+
const oLinear = useSharedValue2(0);
|
|
506
|
+
const visible = useSharedValue2(0);
|
|
507
|
+
const dx = useSharedValue2(0);
|
|
508
|
+
const dy = useSharedValue2(0);
|
|
509
|
+
const pinch = useSharedValue2(1);
|
|
510
|
+
const twist = useSharedValue2(0);
|
|
511
|
+
const activeCount = useSharedValue2(0);
|
|
512
|
+
const capture = useMemo4(
|
|
513
|
+
() => () => {
|
|
514
|
+
const from = live.current;
|
|
515
|
+
if (!from || origin.current) return;
|
|
516
|
+
origin.current = from;
|
|
517
|
+
oX.value = from.x * width;
|
|
518
|
+
oY.value = from.y * height;
|
|
519
|
+
oR.value = from.radius * width;
|
|
520
|
+
oRot.value = from.rotation;
|
|
521
|
+
oLinear.value = from.mode === "linear" ? 1 : 0;
|
|
522
|
+
},
|
|
523
|
+
[width, height, oX, oY, oR, oRot, oLinear]
|
|
524
|
+
);
|
|
525
|
+
const commit = useMemo4(
|
|
526
|
+
() => (tx, ty, scale, rotation) => {
|
|
527
|
+
const from = origin.current;
|
|
528
|
+
origin.current = null;
|
|
529
|
+
if (!from || !layer || width <= 0 || height <= 0) return;
|
|
530
|
+
let next = moveTiltCenter(from, tx / width, ty / height);
|
|
531
|
+
next = scaleTiltRadius({ ...next, radius: from.radius, feather: from.feather }, scale);
|
|
532
|
+
next = rotateTiltBand({ ...next, rotation: from.rotation }, rotation);
|
|
533
|
+
dispatch({
|
|
534
|
+
type: "layer/patchData",
|
|
535
|
+
id: layer.id,
|
|
536
|
+
patch: tiltShiftPatch(next)
|
|
537
|
+
});
|
|
538
|
+
},
|
|
539
|
+
[dispatch, layer, width, height]
|
|
540
|
+
);
|
|
541
|
+
const gesture = useMemo4(() => {
|
|
542
|
+
const begin = () => {
|
|
543
|
+
"worklet";
|
|
544
|
+
activeCount.value += 1;
|
|
545
|
+
visible.value = 1;
|
|
546
|
+
runOnJS3(capture)();
|
|
547
|
+
};
|
|
548
|
+
const end = () => {
|
|
549
|
+
"worklet";
|
|
550
|
+
activeCount.value = Math.max(0, activeCount.value - 1);
|
|
551
|
+
if (activeCount.value > 0) return;
|
|
552
|
+
visible.value = 0;
|
|
553
|
+
runOnJS3(commit)(dx.value, dy.value, pinch.value, twist.value);
|
|
554
|
+
dx.value = 0;
|
|
555
|
+
dy.value = 0;
|
|
556
|
+
pinch.value = 1;
|
|
557
|
+
twist.value = 0;
|
|
558
|
+
};
|
|
559
|
+
const pan = Gesture3.Pan().enabled(active).onStart(begin).onUpdate((e) => {
|
|
560
|
+
"worklet";
|
|
561
|
+
dx.value = e.translationX;
|
|
562
|
+
dy.value = e.translationY;
|
|
563
|
+
}).onFinalize(end);
|
|
564
|
+
const pinchGesture = Gesture3.Pinch().enabled(active).onStart(begin).onUpdate((e) => {
|
|
565
|
+
"worklet";
|
|
566
|
+
pinch.value = e.scale;
|
|
567
|
+
}).onFinalize(end);
|
|
568
|
+
const rotation = Gesture3.Rotation().enabled(active).onStart(begin).onUpdate((e) => {
|
|
569
|
+
"worklet";
|
|
570
|
+
twist.value = e.rotation;
|
|
571
|
+
}).onFinalize(end);
|
|
572
|
+
return Gesture3.Simultaneous(pan, pinchGesture, rotation);
|
|
573
|
+
}, [active, capture, commit, activeCount, visible, dx, dy, pinch, twist]);
|
|
574
|
+
const ringStyle = useAnimatedStyle(() => {
|
|
575
|
+
const r = Math.max(1, oR.value * pinch.value);
|
|
576
|
+
const cx = oX.value + dx.value;
|
|
577
|
+
const cy = oY.value + dy.value;
|
|
578
|
+
const band = oLinear.value > 0.5;
|
|
579
|
+
return {
|
|
580
|
+
opacity: visible.value,
|
|
581
|
+
left: band ? -width : cx - r,
|
|
582
|
+
top: cy - r,
|
|
583
|
+
width: band ? width * 3 : r * 2,
|
|
584
|
+
height: r * 2,
|
|
585
|
+
borderRadius: band ? 0 : r,
|
|
586
|
+
transform: [{ rotate: `${oRot.value + twist.value}rad` }]
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
if (!active) return null;
|
|
590
|
+
return /* @__PURE__ */ jsx6(GestureDetector3, { gesture, children: /* @__PURE__ */ jsx6(Animated3.View, { style: StyleSheet3.absoluteFill, children: /* @__PURE__ */ jsx6(Animated3.View, { pointerEvents: "none", style: [styles.ring, ringStyle] }) }) });
|
|
591
|
+
}
|
|
592
|
+
var styles = StyleSheet3.create({
|
|
593
|
+
ring: {
|
|
594
|
+
position: "absolute",
|
|
595
|
+
borderWidth: 1.5,
|
|
596
|
+
borderColor: "rgba(255,255,255,0.9)"
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// src/gestures/CropGrid.tsx
|
|
601
|
+
import React7 from "react";
|
|
602
|
+
import { StyleSheet as StyleSheet4, View } from "react-native";
|
|
603
|
+
import { jsx as jsx7, jsxs } from "react/jsx-runtime";
|
|
604
|
+
function CropGrid({ always = false }) {
|
|
605
|
+
const { cropActive } = useInteractionState();
|
|
606
|
+
if (!always && !cropActive) return null;
|
|
607
|
+
return /* @__PURE__ */ jsx7(View, { pointerEvents: "none", style: StyleSheet4.absoluteFill, children: [1 / 3, 2 / 3].map((fraction) => /* @__PURE__ */ jsxs(React7.Fragment, { children: [
|
|
608
|
+
/* @__PURE__ */ jsx7(View, { style: [styles2.vertical, { left: `${fraction * 100}%` }] }),
|
|
609
|
+
/* @__PURE__ */ jsx7(View, { style: [styles2.horizontal, { top: `${fraction * 100}%` }] })
|
|
610
|
+
] }, fraction)) });
|
|
611
|
+
}
|
|
612
|
+
var styles2 = StyleSheet4.create({
|
|
613
|
+
vertical: { position: "absolute", top: 0, bottom: 0, width: 1, backgroundColor: "#ffffff88" },
|
|
614
|
+
horizontal: { position: "absolute", left: 0, right: 0, height: 1, backgroundColor: "#ffffff88" }
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// src/gestures/LayerGestureSurface.tsx
|
|
618
|
+
import {
|
|
619
|
+
deleteTargetFromProfile,
|
|
620
|
+
hitTestDocument,
|
|
621
|
+
isDirectManipulationLayer,
|
|
622
|
+
isSameTransform,
|
|
623
|
+
layerNaturalSize,
|
|
624
|
+
layerGestureConfigFromProfile,
|
|
625
|
+
NO_SNAP as NO_SNAP2,
|
|
626
|
+
useEditor as useEditor5,
|
|
627
|
+
useEditorStore as useEditorStore2
|
|
628
|
+
} from "@pictor-sdk/core";
|
|
629
|
+
import { preferMeasuredSize, useCanvasMetrics as useCanvasMetrics4, useRenderer as useRenderer2 } from "@pictor-sdk/skia";
|
|
630
|
+
import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo5, useRef as useRef7, useState as useState2 } from "react";
|
|
631
|
+
import { StyleSheet as StyleSheet5 } from "react-native";
|
|
632
|
+
import { Gesture as Gesture4, GestureDetector as GestureDetector4 } from "react-native-gesture-handler";
|
|
633
|
+
import Animated4, { useDerivedValue as useDerivedValue2, useSharedValue as useSharedValue3 } from "react-native-reanimated";
|
|
634
|
+
import { runOnJS as runOnJS4 } from "react-native-worklets";
|
|
635
|
+
|
|
636
|
+
// src/gestures/layerGestureWorklet.ts
|
|
637
|
+
function composeLayerGestureUI(base, input, aspect, config, previous, settle) {
|
|
638
|
+
"worklet";
|
|
639
|
+
const requested = base.scale * input.pinch;
|
|
640
|
+
let scale;
|
|
641
|
+
if (settle) {
|
|
642
|
+
scale = Math.min(config.maxScale, Math.max(config.minScale, requested));
|
|
643
|
+
} else if (requested < config.minScale) {
|
|
644
|
+
scale = config.minScale - (config.minScale - requested) * config.scaleResistance;
|
|
645
|
+
} else if (requested > config.maxScale) {
|
|
646
|
+
scale = config.maxScale + (requested - config.maxScale) * config.scaleResistance;
|
|
647
|
+
} else {
|
|
648
|
+
scale = requested;
|
|
649
|
+
}
|
|
650
|
+
const ratio = scale / Math.max(base.scale, 1e-9);
|
|
651
|
+
const rotationDelta = config.rotationEnabled ? input.rotate : 0;
|
|
652
|
+
const dx = base.x - input.focalX;
|
|
653
|
+
const dy = (base.y - input.focalY) * aspect;
|
|
654
|
+
const cos = Math.cos(rotationDelta);
|
|
655
|
+
const sin = Math.sin(rotationDelta);
|
|
656
|
+
const rx = (dx * cos - dy * sin) * ratio;
|
|
657
|
+
const ry = (dx * sin + dy * cos) * ratio;
|
|
658
|
+
let x = input.focalX + rx + input.panX;
|
|
659
|
+
let y = input.focalY + ry / aspect + input.panY;
|
|
660
|
+
let rotation = base.rotation + rotationDelta;
|
|
661
|
+
let snapCenterX = false;
|
|
662
|
+
let snapCenterY = false;
|
|
663
|
+
let snapRotation2 = false;
|
|
664
|
+
if (config.centerSnap !== false) {
|
|
665
|
+
const engage = config.centerSnap;
|
|
666
|
+
const release = Math.max(engage, config.centerSnapRelease);
|
|
667
|
+
const tx = previous.centerX ? release : engage;
|
|
668
|
+
if (Math.abs(x - 0.5) <= tx + 1e-9) {
|
|
669
|
+
x = 0.5;
|
|
670
|
+
snapCenterX = true;
|
|
671
|
+
}
|
|
672
|
+
const ty = previous.centerY ? release : engage;
|
|
673
|
+
if (Math.abs(y - 0.5) <= ty + 1e-9) {
|
|
674
|
+
y = 0.5;
|
|
675
|
+
snapCenterY = true;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (config.rotationSnap !== false && config.rotationEnabled) {
|
|
679
|
+
const engage = config.rotationSnap;
|
|
680
|
+
const release = Math.max(engage, config.rotationSnapRelease);
|
|
681
|
+
const step = Math.PI / 2;
|
|
682
|
+
const nearest = Math.round(rotation / step) * step;
|
|
683
|
+
const tr = previous.rotation ? release : engage;
|
|
684
|
+
if (Math.abs(rotation - nearest) <= tr + 1e-9) {
|
|
685
|
+
rotation = nearest;
|
|
686
|
+
snapRotation2 = true;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return { x, y, scale, rotation, snapCenterX, snapCenterY, snapRotation: snapRotation2 };
|
|
690
|
+
}
|
|
691
|
+
function isOverDeleteTargetUI(x, y, target, aspect) {
|
|
692
|
+
"worklet";
|
|
693
|
+
const dx = x - target.x;
|
|
694
|
+
const dy = (y - target.y) * aspect;
|
|
695
|
+
return Math.sqrt(dx * dx + dy * dy) <= target.radius;
|
|
696
|
+
}
|
|
697
|
+
function layerPreviewDeltaUI(committed, live, width, height) {
|
|
698
|
+
"worklet";
|
|
699
|
+
return [
|
|
700
|
+
{ translateX: live.x * width },
|
|
701
|
+
{ translateY: live.y * height },
|
|
702
|
+
{ rotate: live.rotation - committed.rotation },
|
|
703
|
+
{ scale: live.scale / Math.max(committed.scale, 1e-9) },
|
|
704
|
+
{ translateX: -committed.x * width },
|
|
705
|
+
{ translateY: -committed.y * height }
|
|
706
|
+
];
|
|
707
|
+
}
|
|
708
|
+
function hitTestLayerCandidatesUI(x, y, candidates, documentWidth, documentHeight) {
|
|
709
|
+
"worklet";
|
|
710
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
711
|
+
const candidate = candidates[i];
|
|
712
|
+
let dx = x * documentWidth - candidate.x * documentWidth;
|
|
713
|
+
let dy = y * documentHeight - candidate.y * documentHeight;
|
|
714
|
+
const cos = Math.cos(-candidate.rotation);
|
|
715
|
+
const sin = Math.sin(-candidate.rotation);
|
|
716
|
+
const rx = dx * cos - dy * sin;
|
|
717
|
+
const ry = dx * sin + dy * cos;
|
|
718
|
+
dx = candidate.flipX ? -rx : rx;
|
|
719
|
+
dy = candidate.flipY ? -ry : ry;
|
|
720
|
+
const width = candidate.scale * candidate.width * documentWidth;
|
|
721
|
+
const height = candidate.scale * candidate.height * documentWidth;
|
|
722
|
+
if (width <= 0 || height <= 0) continue;
|
|
723
|
+
const localX = dx / width + candidate.anchorX;
|
|
724
|
+
const localY = dy / height + candidate.anchorY;
|
|
725
|
+
if (localX >= 0 && localX <= 1 && localY >= 0 && localY <= 1) return candidate;
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/gestures/LayerGestureSurface.tsx
|
|
731
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
732
|
+
function LayerGestureSurface({ onSnap }) {
|
|
733
|
+
const { document, dispatch, profile } = useEditor5();
|
|
734
|
+
const store = useEditorStore2();
|
|
735
|
+
const metrics = useCanvasMetrics4();
|
|
736
|
+
const { setPreview, setLayerDragActive, setOverDeleteTarget, setSnap } = useInteractionState();
|
|
737
|
+
const renderer = useRenderer2();
|
|
738
|
+
const config = useMemo5(
|
|
739
|
+
() => layerGestureConfigFromProfile(profile.interaction?.layers),
|
|
740
|
+
[profile]
|
|
741
|
+
);
|
|
742
|
+
const deleteTarget = useMemo5(
|
|
743
|
+
() => deleteTargetFromProfile(profile.interaction?.layers),
|
|
744
|
+
[profile]
|
|
745
|
+
);
|
|
746
|
+
const aspect = document.size.height / document.size.width;
|
|
747
|
+
const [measureVersion, setMeasureVersion] = useState2(0);
|
|
748
|
+
useEffect2(
|
|
749
|
+
() => renderer.textMeasures.subscribe(() => setMeasureVersion((v) => v + 1)),
|
|
750
|
+
[renderer]
|
|
751
|
+
);
|
|
752
|
+
const hitCandidates = useMemo5(
|
|
753
|
+
() => document.layers.map((layer, index) => ({ layer, index })).filter(
|
|
754
|
+
({ layer, index }) => !layer.hidden && !layer.locked && isDirectManipulationLayer(layer, index, profile.interaction?.layers)
|
|
755
|
+
).reverse().map(({ layer }) => {
|
|
756
|
+
const natural = preferMeasuredSize(
|
|
757
|
+
renderer.textMeasures.get(layer.id),
|
|
758
|
+
layerNaturalSize(layer, store.registry, document)
|
|
759
|
+
);
|
|
760
|
+
const anchor = layer.transform.anchor ?? { x: 0.5, y: 0.5 };
|
|
761
|
+
return {
|
|
762
|
+
id: layer.id,
|
|
763
|
+
x: layer.transform.x,
|
|
764
|
+
y: layer.transform.y,
|
|
765
|
+
scale: layer.transform.scale,
|
|
766
|
+
rotation: layer.transform.rotation,
|
|
767
|
+
width: natural.width,
|
|
768
|
+
height: natural.height,
|
|
769
|
+
anchorX: anchor.x,
|
|
770
|
+
anchorY: anchor.y,
|
|
771
|
+
flipX: layer.transform.flipX ?? false,
|
|
772
|
+
flipY: layer.transform.flipY ?? false
|
|
773
|
+
};
|
|
774
|
+
}),
|
|
775
|
+
// `measureVersion` is deliberate: the measurements live outside React, so
|
|
776
|
+
// this is how a new one re-derives the hit geometry.
|
|
777
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
778
|
+
[document, profile, store.registry, renderer, measureVersion]
|
|
779
|
+
);
|
|
780
|
+
const liveX = useSharedValue3(0.5);
|
|
781
|
+
const liveY = useSharedValue3(0.5);
|
|
782
|
+
const liveScale = useSharedValue3(1);
|
|
783
|
+
const liveRotation = useSharedValue3(0);
|
|
784
|
+
const committedX = useSharedValue3(0.5);
|
|
785
|
+
const committedY = useSharedValue3(0.5);
|
|
786
|
+
const committedScale = useSharedValue3(1);
|
|
787
|
+
const committedRotation = useSharedValue3(0);
|
|
788
|
+
const baseX = useSharedValue3(0.5);
|
|
789
|
+
const baseY = useSharedValue3(0.5);
|
|
790
|
+
const baseScale = useSharedValue3(1);
|
|
791
|
+
const baseRotation = useSharedValue3(0);
|
|
792
|
+
const originPanX = useSharedValue3(0);
|
|
793
|
+
const originPanY = useSharedValue3(0);
|
|
794
|
+
const originPinch = useSharedValue3(1);
|
|
795
|
+
const originRotate = useSharedValue3(0);
|
|
796
|
+
const rawPanX = useSharedValue3(0);
|
|
797
|
+
const rawPanY = useSharedValue3(0);
|
|
798
|
+
const rawPinch = useSharedValue3(1);
|
|
799
|
+
const rawRotate = useSharedValue3(0);
|
|
800
|
+
const focalX = useSharedValue3(0.5);
|
|
801
|
+
const focalY = useSharedValue3(0.5);
|
|
802
|
+
const pointers = useSharedValue3(0);
|
|
803
|
+
const active = useSharedValue3(false);
|
|
804
|
+
const snapCX = useSharedValue3(false);
|
|
805
|
+
const snapCY = useSharedValue3(false);
|
|
806
|
+
const snapR = useSharedValue3(false);
|
|
807
|
+
const overDelete = useSharedValue3(false);
|
|
808
|
+
const previewTransform = useDerivedValue2(() => {
|
|
809
|
+
return layerPreviewDeltaUI(
|
|
810
|
+
{
|
|
811
|
+
x: committedX.value,
|
|
812
|
+
y: committedY.value,
|
|
813
|
+
scale: committedScale.value,
|
|
814
|
+
rotation: committedRotation.value
|
|
815
|
+
},
|
|
816
|
+
{ x: liveX.value, y: liveY.value, scale: liveScale.value, rotation: liveRotation.value },
|
|
817
|
+
document.size.width,
|
|
818
|
+
document.size.height
|
|
819
|
+
);
|
|
820
|
+
}, [document.size.width, document.size.height]);
|
|
821
|
+
const latest = useRef7({ document, metrics, store, dispatch, onSnap, profile });
|
|
822
|
+
latest.current = { document, metrics, store, dispatch, onSnap, profile };
|
|
823
|
+
const grabbedId = useRef7(null);
|
|
824
|
+
const grabbedFrom = useRef7(null);
|
|
825
|
+
const snapState = useRef7(NO_SNAP2);
|
|
826
|
+
const onGrabLayer = useCallback3((id) => {
|
|
827
|
+
const { document: doc, store: s } = latest.current;
|
|
828
|
+
const hit = doc.layers.find((layer) => layer.id === id);
|
|
829
|
+
if (!hit) return;
|
|
830
|
+
const t = hit.transform;
|
|
831
|
+
grabbedId.current = hit.id;
|
|
832
|
+
grabbedFrom.current = { ...t };
|
|
833
|
+
s.select([hit.id]);
|
|
834
|
+
setPreview({ layerId: hit.id, transform: previewTransform });
|
|
835
|
+
setLayerDragActive(true);
|
|
836
|
+
}, [setPreview, setLayerDragActive, previewTransform]);
|
|
837
|
+
const onSelectAt = useCallback3((xDp, yDp) => {
|
|
838
|
+
const { document: doc, metrics: m, store: s } = latest.current;
|
|
839
|
+
if (!m) return;
|
|
840
|
+
const layerInteraction = latest.current.profile.interaction?.layers;
|
|
841
|
+
const hit = hitTestDocument(
|
|
842
|
+
doc,
|
|
843
|
+
{ x: xDp / m.width, y: yDp / m.height },
|
|
844
|
+
s.registry,
|
|
845
|
+
(layer, index) => isDirectManipulationLayer(layer, index, layerInteraction)
|
|
846
|
+
);
|
|
847
|
+
s.select(hit && !hit.locked ? [hit.id] : []);
|
|
848
|
+
}, []);
|
|
849
|
+
const onSnapChanged = useCallback3(
|
|
850
|
+
(centerX, centerY, rotation) => {
|
|
851
|
+
const previous = snapState.current;
|
|
852
|
+
const next = { centerX, centerY, rotation };
|
|
853
|
+
snapState.current = next;
|
|
854
|
+
const notify = latest.current.onSnap;
|
|
855
|
+
if (centerX && !previous.centerX) notify?.({ kind: "centerX" });
|
|
856
|
+
if (centerY && !previous.centerY) notify?.({ kind: "centerY" });
|
|
857
|
+
if (rotation && !previous.rotation) notify?.({ kind: "rotation" });
|
|
858
|
+
setSnap(next);
|
|
859
|
+
},
|
|
860
|
+
[setSnap]
|
|
861
|
+
);
|
|
862
|
+
const onHoverChanged = useCallback3(
|
|
863
|
+
(over) => setOverDeleteTarget(over),
|
|
864
|
+
[setOverDeleteTarget]
|
|
865
|
+
);
|
|
866
|
+
const onGestureFinished = useCallback3(
|
|
867
|
+
(t, deleted) => {
|
|
868
|
+
const { dispatch: d, store: s } = latest.current;
|
|
869
|
+
const id = grabbedId.current;
|
|
870
|
+
const from = grabbedFrom.current;
|
|
871
|
+
grabbedId.current = null;
|
|
872
|
+
grabbedFrom.current = null;
|
|
873
|
+
snapState.current = NO_SNAP2;
|
|
874
|
+
setLayerDragActive(false);
|
|
875
|
+
setOverDeleteTarget(false);
|
|
876
|
+
setSnap(NO_SNAP2);
|
|
877
|
+
setPreview(null);
|
|
878
|
+
if (!id) return;
|
|
879
|
+
if (deleted) {
|
|
880
|
+
d({ type: "layer/remove", id });
|
|
881
|
+
s.select([]);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (from && isSameTransform(from, t)) return;
|
|
885
|
+
d({ type: "layer/transform", id, patch: t });
|
|
886
|
+
},
|
|
887
|
+
[setLayerDragActive, setOverDeleteTarget, setSnap, setPreview]
|
|
888
|
+
);
|
|
889
|
+
useEffect2(() => () => setPreview(null), [setPreview]);
|
|
890
|
+
const gesture = useMemo5(() => {
|
|
891
|
+
const metricsWidth = metrics?.width ?? 0;
|
|
892
|
+
const metricsHeight = metrics?.height ?? 0;
|
|
893
|
+
const target = deleteTarget;
|
|
894
|
+
const candidates = hitCandidates;
|
|
895
|
+
const rebase = () => {
|
|
896
|
+
"worklet";
|
|
897
|
+
baseX.value = liveX.value;
|
|
898
|
+
baseY.value = liveY.value;
|
|
899
|
+
baseScale.value = liveScale.value;
|
|
900
|
+
baseRotation.value = liveRotation.value;
|
|
901
|
+
originPanX.value = rawPanX.value;
|
|
902
|
+
originPanY.value = rawPanY.value;
|
|
903
|
+
originPinch.value = rawPinch.value;
|
|
904
|
+
originRotate.value = rawRotate.value;
|
|
905
|
+
};
|
|
906
|
+
const recompute = (settle) => {
|
|
907
|
+
"worklet";
|
|
908
|
+
if (!active.value || metricsWidth === 0 || metricsHeight === 0) return;
|
|
909
|
+
const out = composeLayerGestureUI(
|
|
910
|
+
{
|
|
911
|
+
x: baseX.value,
|
|
912
|
+
y: baseY.value,
|
|
913
|
+
scale: baseScale.value,
|
|
914
|
+
rotation: baseRotation.value
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
panX: (rawPanX.value - originPanX.value) / metricsWidth,
|
|
918
|
+
panY: (rawPanY.value - originPanY.value) / metricsHeight,
|
|
919
|
+
pinch: rawPinch.value / Math.max(originPinch.value, 1e-9),
|
|
920
|
+
rotate: rawRotate.value - originRotate.value,
|
|
921
|
+
focalX: focalX.value,
|
|
922
|
+
focalY: focalY.value
|
|
923
|
+
},
|
|
924
|
+
aspect,
|
|
925
|
+
config,
|
|
926
|
+
{ centerX: snapCX.value, centerY: snapCY.value, rotation: snapR.value },
|
|
927
|
+
settle
|
|
928
|
+
);
|
|
929
|
+
liveX.value = out.x;
|
|
930
|
+
liveY.value = out.y;
|
|
931
|
+
liveScale.value = out.scale;
|
|
932
|
+
liveRotation.value = out.rotation;
|
|
933
|
+
if (out.snapCenterX !== snapCX.value || out.snapCenterY !== snapCY.value || out.snapRotation !== snapR.value) {
|
|
934
|
+
snapCX.value = out.snapCenterX;
|
|
935
|
+
snapCY.value = out.snapCenterY;
|
|
936
|
+
snapR.value = out.snapRotation;
|
|
937
|
+
runOnJS4(onSnapChanged)(out.snapCenterX, out.snapCenterY, out.snapRotation);
|
|
938
|
+
}
|
|
939
|
+
if (target) {
|
|
940
|
+
const over = isOverDeleteTargetUI(out.x, out.y, target, aspect);
|
|
941
|
+
if (over !== overDelete.value) {
|
|
942
|
+
overDelete.value = over;
|
|
943
|
+
runOnJS4(onHoverChanged)(over);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
const syncPointers = (count) => {
|
|
948
|
+
"worklet";
|
|
949
|
+
if (active.value && count !== pointers.value) {
|
|
950
|
+
pointers.value = count;
|
|
951
|
+
rebase();
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
const endIfIdle = () => {
|
|
955
|
+
"worklet";
|
|
956
|
+
if (!active.value) {
|
|
957
|
+
runOnJS4(onGestureFinished)({ x: 0, y: 0, scale: 1, rotation: 0 }, false);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
recompute(true);
|
|
961
|
+
active.value = false;
|
|
962
|
+
pointers.value = 0;
|
|
963
|
+
const deleted = overDelete.value;
|
|
964
|
+
overDelete.value = false;
|
|
965
|
+
snapCX.value = false;
|
|
966
|
+
snapCY.value = false;
|
|
967
|
+
snapR.value = false;
|
|
968
|
+
runOnJS4(onGestureFinished)(
|
|
969
|
+
{
|
|
970
|
+
x: liveX.value,
|
|
971
|
+
y: liveY.value,
|
|
972
|
+
scale: liveScale.value,
|
|
973
|
+
rotation: liveRotation.value
|
|
974
|
+
},
|
|
975
|
+
deleted
|
|
976
|
+
);
|
|
977
|
+
};
|
|
978
|
+
const pan = Gesture4.Pan().minPointers(1).maxPointers(2).onBegin((e) => {
|
|
979
|
+
"worklet";
|
|
980
|
+
if (active.value) return;
|
|
981
|
+
rawPanX.value = 0;
|
|
982
|
+
rawPanY.value = 0;
|
|
983
|
+
rawPinch.value = 1;
|
|
984
|
+
rawRotate.value = 0;
|
|
985
|
+
originPanX.value = 0;
|
|
986
|
+
originPanY.value = 0;
|
|
987
|
+
originPinch.value = 1;
|
|
988
|
+
originRotate.value = 0;
|
|
989
|
+
pointers.value = e.numberOfPointers;
|
|
990
|
+
const hit = hitTestLayerCandidatesUI(
|
|
991
|
+
e.x / Math.max(metricsWidth, 1e-9),
|
|
992
|
+
e.y / Math.max(metricsHeight, 1e-9),
|
|
993
|
+
candidates,
|
|
994
|
+
document.size.width,
|
|
995
|
+
document.size.height
|
|
996
|
+
);
|
|
997
|
+
if (!hit) {
|
|
998
|
+
active.value = false;
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
liveX.value = hit.x;
|
|
1002
|
+
liveY.value = hit.y;
|
|
1003
|
+
liveScale.value = hit.scale;
|
|
1004
|
+
liveRotation.value = hit.rotation;
|
|
1005
|
+
committedX.value = hit.x;
|
|
1006
|
+
committedY.value = hit.y;
|
|
1007
|
+
committedScale.value = hit.scale;
|
|
1008
|
+
committedRotation.value = hit.rotation;
|
|
1009
|
+
baseX.value = hit.x;
|
|
1010
|
+
baseY.value = hit.y;
|
|
1011
|
+
baseScale.value = hit.scale;
|
|
1012
|
+
baseRotation.value = hit.rotation;
|
|
1013
|
+
focalX.value = hit.x;
|
|
1014
|
+
focalY.value = hit.y;
|
|
1015
|
+
active.value = true;
|
|
1016
|
+
runOnJS4(onGrabLayer)(hit.id);
|
|
1017
|
+
}).onUpdate((e) => {
|
|
1018
|
+
"worklet";
|
|
1019
|
+
rawPanX.value = e.translationX;
|
|
1020
|
+
rawPanY.value = e.translationY;
|
|
1021
|
+
syncPointers(e.numberOfPointers);
|
|
1022
|
+
recompute(false);
|
|
1023
|
+
}).onFinalize(() => {
|
|
1024
|
+
"worklet";
|
|
1025
|
+
endIfIdle();
|
|
1026
|
+
});
|
|
1027
|
+
const pinch = Gesture4.Pinch().onUpdate((e) => {
|
|
1028
|
+
"worklet";
|
|
1029
|
+
rawPinch.value = e.scale;
|
|
1030
|
+
focalX.value = e.focalX / metricsWidth;
|
|
1031
|
+
focalY.value = e.focalY / metricsHeight;
|
|
1032
|
+
syncPointers(e.numberOfPointers);
|
|
1033
|
+
recompute(false);
|
|
1034
|
+
});
|
|
1035
|
+
const rotate = Gesture4.Rotation().enabled(config.rotationEnabled).onUpdate((e) => {
|
|
1036
|
+
"worklet";
|
|
1037
|
+
rawRotate.value = e.rotation;
|
|
1038
|
+
focalX.value = e.anchorX / metricsWidth;
|
|
1039
|
+
focalY.value = e.anchorY / metricsHeight;
|
|
1040
|
+
syncPointers(e.numberOfPointers);
|
|
1041
|
+
recompute(false);
|
|
1042
|
+
});
|
|
1043
|
+
const tap = Gesture4.Tap().onEnd((e) => {
|
|
1044
|
+
"worklet";
|
|
1045
|
+
runOnJS4(onSelectAt)(e.x, e.y);
|
|
1046
|
+
});
|
|
1047
|
+
return Gesture4.Simultaneous(Gesture4.Race(pan, tap), pinch, rotate);
|
|
1048
|
+
}, [
|
|
1049
|
+
metrics,
|
|
1050
|
+
config,
|
|
1051
|
+
deleteTarget,
|
|
1052
|
+
aspect,
|
|
1053
|
+
onGrabLayer,
|
|
1054
|
+
onSelectAt,
|
|
1055
|
+
onSnapChanged,
|
|
1056
|
+
onHoverChanged,
|
|
1057
|
+
onGestureFinished,
|
|
1058
|
+
hitCandidates,
|
|
1059
|
+
document.size.width,
|
|
1060
|
+
document.size.height
|
|
1061
|
+
]);
|
|
1062
|
+
return /* @__PURE__ */ jsx8(GestureDetector4, { gesture, children: /* @__PURE__ */ jsx8(Animated4.View, { style: StyleSheet5.absoluteFill }) });
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/gestures/StoryMediaGestureSurface.tsx
|
|
1066
|
+
import {
|
|
1067
|
+
doubleTapMediaTransform,
|
|
1068
|
+
isBaseImageLayer,
|
|
1069
|
+
isSameTransform as isSameTransform3,
|
|
1070
|
+
mediaConfigFromProfile,
|
|
1071
|
+
useEditor as useEditor6
|
|
1072
|
+
} from "@pictor-sdk/core";
|
|
1073
|
+
import { preferResolvedSize, useCanvasMetrics as useCanvasMetrics5, useResolvedImageSize } from "@pictor-sdk/skia";
|
|
1074
|
+
import { useCallback as useCallback4, useEffect as useEffect3, useMemo as useMemo6, useRef as useRef8 } from "react";
|
|
1075
|
+
import { StyleSheet as StyleSheet6 } from "react-native";
|
|
1076
|
+
import { Gesture as Gesture5, GestureDetector as GestureDetector5 } from "react-native-gesture-handler";
|
|
1077
|
+
import Animated5, {
|
|
1078
|
+
cancelAnimation,
|
|
1079
|
+
useDerivedValue as useDerivedValue3,
|
|
1080
|
+
useSharedValue as useSharedValue4,
|
|
1081
|
+
withTiming as withTiming2
|
|
1082
|
+
} from "react-native-reanimated";
|
|
1083
|
+
import { runOnJS as runOnJS5 } from "react-native-worklets";
|
|
1084
|
+
|
|
1085
|
+
// src/gestures/mediaGestureWorklet.ts
|
|
1086
|
+
function mediaFitScaleUI(intrinsic, canvas, fit, rotation) {
|
|
1087
|
+
"worklet";
|
|
1088
|
+
if (intrinsic.width <= 0 || intrinsic.height <= 0) return 1;
|
|
1089
|
+
if (fit === "width") return 1;
|
|
1090
|
+
const w = canvas.width;
|
|
1091
|
+
const h = canvas.width * (intrinsic.height / intrinsic.width);
|
|
1092
|
+
const cos = Math.abs(Math.cos(rotation));
|
|
1093
|
+
const sin = Math.abs(Math.sin(rotation));
|
|
1094
|
+
const byWidth = canvas.width / (w * cos + h * sin);
|
|
1095
|
+
const byHeight = canvas.height / (w * sin + h * cos);
|
|
1096
|
+
return fit === "cover" ? Math.max(byWidth, byHeight) : Math.min(byWidth, byHeight);
|
|
1097
|
+
}
|
|
1098
|
+
function mediaPanBoundsUI(_intrinsic, _canvas, _scale, _rotation) {
|
|
1099
|
+
"worklet";
|
|
1100
|
+
return { minX: 0, maxX: 1, minY: 0, maxY: 1 };
|
|
1101
|
+
}
|
|
1102
|
+
function composeMediaGestureUI(base, input, canvas, intrinsic, config, settle) {
|
|
1103
|
+
"worklet";
|
|
1104
|
+
const rotationDelta = config.rotationEnabled ? input.rotate : 0;
|
|
1105
|
+
const rotation = base.rotation + rotationDelta;
|
|
1106
|
+
const fit = mediaFitScaleUI(intrinsic, canvas, config.fit, rotation);
|
|
1107
|
+
const minScale = fit * config.minScaleFactor;
|
|
1108
|
+
const maxScale = Math.max(minScale, config.maxScale);
|
|
1109
|
+
const requested = base.scale * input.pinch;
|
|
1110
|
+
let scale;
|
|
1111
|
+
if (settle) {
|
|
1112
|
+
scale = Math.min(maxScale, Math.max(minScale, requested));
|
|
1113
|
+
} else if (requested < minScale) {
|
|
1114
|
+
scale = minScale - (minScale - requested) * config.resistance;
|
|
1115
|
+
} else if (requested > maxScale) {
|
|
1116
|
+
scale = maxScale + (requested - maxScale) * config.resistance;
|
|
1117
|
+
} else {
|
|
1118
|
+
scale = requested;
|
|
1119
|
+
}
|
|
1120
|
+
const aspect = canvas.height / canvas.width;
|
|
1121
|
+
const ratio = scale / Math.max(base.scale, 1e-9);
|
|
1122
|
+
const dx = base.x - input.focalX;
|
|
1123
|
+
const dy = (base.y - input.focalY) * aspect;
|
|
1124
|
+
const cos = Math.cos(rotationDelta);
|
|
1125
|
+
const sin = Math.sin(rotationDelta);
|
|
1126
|
+
const rx = (dx * cos - dy * sin) * ratio;
|
|
1127
|
+
const ry = (dx * sin + dy * cos) * ratio;
|
|
1128
|
+
const rawX = input.focalX + rx + input.panX;
|
|
1129
|
+
const rawY = input.focalY + ry / aspect + input.panY;
|
|
1130
|
+
const bounds = mediaPanBoundsUI(intrinsic, canvas, scale, rotation);
|
|
1131
|
+
let x;
|
|
1132
|
+
let y;
|
|
1133
|
+
if (settle) {
|
|
1134
|
+
x = Math.min(bounds.maxX, Math.max(bounds.minX, rawX));
|
|
1135
|
+
y = Math.min(bounds.maxY, Math.max(bounds.minY, rawY));
|
|
1136
|
+
} else {
|
|
1137
|
+
x = rawX < bounds.minX ? bounds.minX - (bounds.minX - rawX) * config.resistance : rawX > bounds.maxX ? bounds.maxX + (rawX - bounds.maxX) * config.resistance : rawX;
|
|
1138
|
+
y = rawY < bounds.minY ? bounds.minY - (bounds.minY - rawY) * config.resistance : rawY > bounds.maxY ? bounds.maxY + (rawY - bounds.maxY) * config.resistance : rawY;
|
|
1139
|
+
}
|
|
1140
|
+
return { x, y, scale, rotation };
|
|
1141
|
+
}
|
|
1142
|
+
function onRecognizerBegin(count) {
|
|
1143
|
+
"worklet";
|
|
1144
|
+
const next = Math.max(0, count) + 1;
|
|
1145
|
+
return { count: next, isFirst: next === 1 };
|
|
1146
|
+
}
|
|
1147
|
+
function onRecognizerEnd(count) {
|
|
1148
|
+
"worklet";
|
|
1149
|
+
const next = Math.max(0, count - 1);
|
|
1150
|
+
return { count: next, isLast: next === 0 && count > 0 };
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// src/gestures/settleCoordinator.ts
|
|
1154
|
+
function createSettleCoordinator(commit) {
|
|
1155
|
+
let generation = 0;
|
|
1156
|
+
let settling = false;
|
|
1157
|
+
return {
|
|
1158
|
+
settle(target, animate) {
|
|
1159
|
+
generation += 1;
|
|
1160
|
+
const mine = generation;
|
|
1161
|
+
settling = true;
|
|
1162
|
+
animate((finished) => {
|
|
1163
|
+
if (mine !== generation) return;
|
|
1164
|
+
generation += 1;
|
|
1165
|
+
settling = false;
|
|
1166
|
+
if (finished) commit(target);
|
|
1167
|
+
});
|
|
1168
|
+
},
|
|
1169
|
+
cancel() {
|
|
1170
|
+
generation += 1;
|
|
1171
|
+
settling = false;
|
|
1172
|
+
},
|
|
1173
|
+
isSettling() {
|
|
1174
|
+
return settling;
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/gestures/transformOwnership.ts
|
|
1180
|
+
import { isSameTransform as isSameTransform2 } from "@pictor-sdk/core";
|
|
1181
|
+
var IDLE_OWNERSHIP = { owning: false, pending: null, previous: null };
|
|
1182
|
+
function beginOwnership() {
|
|
1183
|
+
return { owning: true, pending: null, previous: null };
|
|
1184
|
+
}
|
|
1185
|
+
function commitOwnership(target, dispatched, previous) {
|
|
1186
|
+
return dispatched ? { owning: true, pending: target, previous: previous ?? null } : IDLE_OWNERSHIP;
|
|
1187
|
+
}
|
|
1188
|
+
function documentChanged(state, committed) {
|
|
1189
|
+
if (!committed) return { state, reseed: false };
|
|
1190
|
+
if (state.pending) {
|
|
1191
|
+
if (isSameTransform2(state.pending, committed)) return { state: IDLE_OWNERSHIP, reseed: false };
|
|
1192
|
+
if (state.previous && isSameTransform2(state.previous, committed)) {
|
|
1193
|
+
return { state, reseed: false };
|
|
1194
|
+
}
|
|
1195
|
+
return { state: IDLE_OWNERSHIP, reseed: true };
|
|
1196
|
+
}
|
|
1197
|
+
if (state.owning) return { state, reseed: false };
|
|
1198
|
+
return { state, reseed: true };
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/gestures/StoryMediaGestureSurface.tsx
|
|
1202
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
1203
|
+
function useBaseMediaLayer() {
|
|
1204
|
+
const { document } = useEditor6();
|
|
1205
|
+
const base = document.layers[0];
|
|
1206
|
+
return base && isBaseImageLayer(document.layers, base.id) ? base : void 0;
|
|
1207
|
+
}
|
|
1208
|
+
function StoryMediaGestureSurface({
|
|
1209
|
+
onInteractionChange
|
|
1210
|
+
}) {
|
|
1211
|
+
const { document, dispatch, profile } = useEditor6();
|
|
1212
|
+
const metrics = useCanvasMetrics5();
|
|
1213
|
+
const { setPreview } = useInteractionState();
|
|
1214
|
+
const layer = useBaseMediaLayer();
|
|
1215
|
+
const config = useMemo6(
|
|
1216
|
+
() => mediaConfigFromProfile(profile.interaction?.media),
|
|
1217
|
+
[profile]
|
|
1218
|
+
);
|
|
1219
|
+
const declared = layer?.data?.intrinsic;
|
|
1220
|
+
const resolved = useResolvedImageSize(layer?.data?.source);
|
|
1221
|
+
const intrinsic = useMemo6(
|
|
1222
|
+
() => preferResolvedSize(resolved, declared),
|
|
1223
|
+
[resolved, declared]
|
|
1224
|
+
);
|
|
1225
|
+
const canvas = document.size;
|
|
1226
|
+
const liveX = useSharedValue4(0.5);
|
|
1227
|
+
const liveY = useSharedValue4(0.5);
|
|
1228
|
+
const liveScale = useSharedValue4(1);
|
|
1229
|
+
const liveRotation = useSharedValue4(0);
|
|
1230
|
+
const baseX = useSharedValue4(0.5);
|
|
1231
|
+
const baseY = useSharedValue4(0.5);
|
|
1232
|
+
const baseScale = useSharedValue4(1);
|
|
1233
|
+
const baseRotation = useSharedValue4(0);
|
|
1234
|
+
const originPanX = useSharedValue4(0);
|
|
1235
|
+
const originPanY = useSharedValue4(0);
|
|
1236
|
+
const originPinch = useSharedValue4(1);
|
|
1237
|
+
const originRotate = useSharedValue4(0);
|
|
1238
|
+
const rawPanX = useSharedValue4(0);
|
|
1239
|
+
const rawPanY = useSharedValue4(0);
|
|
1240
|
+
const rawPinch = useSharedValue4(1);
|
|
1241
|
+
const rawRotate = useSharedValue4(0);
|
|
1242
|
+
const focalX = useSharedValue4(0.5);
|
|
1243
|
+
const focalY = useSharedValue4(0.5);
|
|
1244
|
+
const active = useSharedValue4(false);
|
|
1245
|
+
const recognizers = useSharedValue4(0);
|
|
1246
|
+
const startedPan = useSharedValue4(false);
|
|
1247
|
+
const startedPinch = useSharedValue4(false);
|
|
1248
|
+
const startedRotate = useSharedValue4(false);
|
|
1249
|
+
const settleProgress = useSharedValue4(0);
|
|
1250
|
+
const committed = layer?.transform;
|
|
1251
|
+
const ownership = useRef8(IDLE_OWNERSHIP);
|
|
1252
|
+
useEffect3(() => {
|
|
1253
|
+
const next = documentChanged(ownership.current, committed);
|
|
1254
|
+
ownership.current = next.state;
|
|
1255
|
+
if (!next.reseed || !committed) return;
|
|
1256
|
+
liveX.value = committed.x;
|
|
1257
|
+
liveY.value = committed.y;
|
|
1258
|
+
liveScale.value = committed.scale;
|
|
1259
|
+
liveRotation.value = committed.rotation;
|
|
1260
|
+
}, [committed?.x, committed?.y, committed?.scale, committed?.rotation]);
|
|
1261
|
+
const baseCommittedX = useSharedValue4(0.5);
|
|
1262
|
+
const baseCommittedY = useSharedValue4(0.5);
|
|
1263
|
+
const baseCommittedScale = useSharedValue4(1);
|
|
1264
|
+
const baseCommittedRotation = useSharedValue4(0);
|
|
1265
|
+
const previewTransform = useDerivedValue3(() => {
|
|
1266
|
+
const ratio = liveScale.value / Math.max(baseCommittedScale.value, 1e-9);
|
|
1267
|
+
return [
|
|
1268
|
+
{ translateX: liveX.value * canvas.width },
|
|
1269
|
+
{ translateY: liveY.value * canvas.height },
|
|
1270
|
+
{ rotate: liveRotation.value - baseCommittedRotation.value },
|
|
1271
|
+
{ scale: ratio },
|
|
1272
|
+
{ translateX: -baseCommittedX.value * canvas.width },
|
|
1273
|
+
{ translateY: -baseCommittedY.value * canvas.height }
|
|
1274
|
+
];
|
|
1275
|
+
}, [canvas.width, canvas.height]);
|
|
1276
|
+
useEffect3(() => () => setPreview(null), [setPreview]);
|
|
1277
|
+
const latest = useRef8({ layer, dispatch, onInteractionChange, previewTransform });
|
|
1278
|
+
latest.current = { layer, dispatch, onInteractionChange, previewTransform };
|
|
1279
|
+
const settleCoordinatorRef = useRef8(null);
|
|
1280
|
+
const beginInteraction = useCallback4(() => {
|
|
1281
|
+
settleCoordinatorRef.current?.cancel();
|
|
1282
|
+
ownership.current = beginOwnership();
|
|
1283
|
+
const current = latest.current.layer;
|
|
1284
|
+
if (current) {
|
|
1285
|
+
baseCommittedX.value = current.transform.x;
|
|
1286
|
+
baseCommittedY.value = current.transform.y;
|
|
1287
|
+
baseCommittedScale.value = current.transform.scale;
|
|
1288
|
+
baseCommittedRotation.value = current.transform.rotation;
|
|
1289
|
+
setPreview({ layerId: current.id, transform: latest.current.previewTransform });
|
|
1290
|
+
}
|
|
1291
|
+
latest.current.onInteractionChange?.(true);
|
|
1292
|
+
}, [setPreview]);
|
|
1293
|
+
const commitNow = useCallback4((next) => {
|
|
1294
|
+
const current = latest.current.layer;
|
|
1295
|
+
setPreview(null);
|
|
1296
|
+
latest.current.onInteractionChange?.(false);
|
|
1297
|
+
const dispatched = current !== void 0 && !isSameTransform3(current.transform, next);
|
|
1298
|
+
ownership.current = commitOwnership(next, dispatched, current?.transform);
|
|
1299
|
+
if (!dispatched || !current) return;
|
|
1300
|
+
latest.current.dispatch({
|
|
1301
|
+
type: "layer/transform",
|
|
1302
|
+
id: current.id,
|
|
1303
|
+
patch: { x: next.x, y: next.y, scale: next.scale, rotation: next.rotation }
|
|
1304
|
+
});
|
|
1305
|
+
}, [setPreview]);
|
|
1306
|
+
const settleCoordinator = useMemo6(
|
|
1307
|
+
() => createSettleCoordinator((target) => commitNow(target)),
|
|
1308
|
+
// commitNow reads through `latest`, so it is stable
|
|
1309
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1310
|
+
[]
|
|
1311
|
+
);
|
|
1312
|
+
settleCoordinatorRef.current = settleCoordinator;
|
|
1313
|
+
const settleTo = useCallback4(
|
|
1314
|
+
(target, duration) => {
|
|
1315
|
+
settleCoordinator.settle(target, (done) => {
|
|
1316
|
+
liveX.value = withTiming2(target.x, { duration });
|
|
1317
|
+
liveY.value = withTiming2(target.y, { duration });
|
|
1318
|
+
liveRotation.value = withTiming2(target.rotation, { duration });
|
|
1319
|
+
liveScale.value = withTiming2(target.scale, { duration });
|
|
1320
|
+
settleProgress.value = 0;
|
|
1321
|
+
settleProgress.value = withTiming2(1, { duration }, (finished) => {
|
|
1322
|
+
"worklet";
|
|
1323
|
+
runOnJS5(done)(finished === true);
|
|
1324
|
+
});
|
|
1325
|
+
});
|
|
1326
|
+
},
|
|
1327
|
+
// shared values are stable refs, so they are deliberately not dependencies
|
|
1328
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1329
|
+
[settleCoordinator]
|
|
1330
|
+
);
|
|
1331
|
+
useEffect3(
|
|
1332
|
+
() => () => {
|
|
1333
|
+
settleCoordinator.cancel();
|
|
1334
|
+
ownership.current = IDLE_OWNERSHIP;
|
|
1335
|
+
cancelAnimation(liveX);
|
|
1336
|
+
cancelAnimation(liveY);
|
|
1337
|
+
cancelAnimation(liveScale);
|
|
1338
|
+
cancelAnimation(liveRotation);
|
|
1339
|
+
cancelAnimation(settleProgress);
|
|
1340
|
+
},
|
|
1341
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1342
|
+
[settleCoordinator]
|
|
1343
|
+
);
|
|
1344
|
+
const runDoubleTap = useCallback4(
|
|
1345
|
+
(xDp, yDp) => {
|
|
1346
|
+
const current = latest.current.layer;
|
|
1347
|
+
if (!current || !metrics) return;
|
|
1348
|
+
const target = doubleTapMediaTransform(current.transform, canvas, intrinsic, config, {
|
|
1349
|
+
x: xDp / metrics.width,
|
|
1350
|
+
y: yDp / metrics.height
|
|
1351
|
+
});
|
|
1352
|
+
beginInteraction();
|
|
1353
|
+
settleTo(target, config.settleMs);
|
|
1354
|
+
},
|
|
1355
|
+
[metrics, canvas, intrinsic, config, settleTo, beginInteraction]
|
|
1356
|
+
);
|
|
1357
|
+
const gesture = useMemo6(() => {
|
|
1358
|
+
const width = metrics?.width ?? 0;
|
|
1359
|
+
const height = metrics?.height ?? 0;
|
|
1360
|
+
const settleMs = config.settleMs;
|
|
1361
|
+
const recompute = () => {
|
|
1362
|
+
"worklet";
|
|
1363
|
+
if (!active.value || width === 0 || height === 0) return;
|
|
1364
|
+
const out = composeMediaGestureUI(
|
|
1365
|
+
{
|
|
1366
|
+
x: baseX.value,
|
|
1367
|
+
y: baseY.value,
|
|
1368
|
+
scale: baseScale.value,
|
|
1369
|
+
rotation: baseRotation.value
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
panX: (rawPanX.value - originPanX.value) / width,
|
|
1373
|
+
panY: (rawPanY.value - originPanY.value) / height,
|
|
1374
|
+
pinch: rawPinch.value / Math.max(originPinch.value, 1e-9),
|
|
1375
|
+
rotate: rawRotate.value - originRotate.value,
|
|
1376
|
+
focalX: focalX.value,
|
|
1377
|
+
focalY: focalY.value
|
|
1378
|
+
},
|
|
1379
|
+
canvas,
|
|
1380
|
+
intrinsic,
|
|
1381
|
+
config,
|
|
1382
|
+
false
|
|
1383
|
+
);
|
|
1384
|
+
liveX.value = out.x;
|
|
1385
|
+
liveY.value = out.y;
|
|
1386
|
+
liveScale.value = out.scale;
|
|
1387
|
+
liveRotation.value = out.rotation;
|
|
1388
|
+
};
|
|
1389
|
+
const begin = () => {
|
|
1390
|
+
"worklet";
|
|
1391
|
+
const started = onRecognizerBegin(recognizers.value);
|
|
1392
|
+
recognizers.value = started.count;
|
|
1393
|
+
if (!started.isFirst) return;
|
|
1394
|
+
cancelAnimation(liveX);
|
|
1395
|
+
cancelAnimation(liveY);
|
|
1396
|
+
cancelAnimation(liveScale);
|
|
1397
|
+
cancelAnimation(liveRotation);
|
|
1398
|
+
cancelAnimation(settleProgress);
|
|
1399
|
+
active.value = true;
|
|
1400
|
+
rawPanX.value = 0;
|
|
1401
|
+
rawPanY.value = 0;
|
|
1402
|
+
rawPinch.value = 1;
|
|
1403
|
+
rawRotate.value = 0;
|
|
1404
|
+
originPanX.value = 0;
|
|
1405
|
+
originPanY.value = 0;
|
|
1406
|
+
originPinch.value = 1;
|
|
1407
|
+
originRotate.value = 0;
|
|
1408
|
+
baseX.value = liveX.value;
|
|
1409
|
+
baseY.value = liveY.value;
|
|
1410
|
+
baseScale.value = liveScale.value;
|
|
1411
|
+
baseRotation.value = liveRotation.value;
|
|
1412
|
+
focalX.value = liveX.value;
|
|
1413
|
+
focalY.value = liveY.value;
|
|
1414
|
+
runOnJS5(beginInteraction)();
|
|
1415
|
+
};
|
|
1416
|
+
const finish = () => {
|
|
1417
|
+
"worklet";
|
|
1418
|
+
const ended = onRecognizerEnd(recognizers.value);
|
|
1419
|
+
recognizers.value = ended.count;
|
|
1420
|
+
if (!ended.isLast || !active.value) return;
|
|
1421
|
+
active.value = false;
|
|
1422
|
+
const settled = composeMediaGestureUI(
|
|
1423
|
+
{
|
|
1424
|
+
x: baseX.value,
|
|
1425
|
+
y: baseY.value,
|
|
1426
|
+
scale: baseScale.value,
|
|
1427
|
+
rotation: baseRotation.value
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
panX: (rawPanX.value - originPanX.value) / width,
|
|
1431
|
+
panY: (rawPanY.value - originPanY.value) / height,
|
|
1432
|
+
pinch: rawPinch.value / Math.max(originPinch.value, 1e-9),
|
|
1433
|
+
rotate: rawRotate.value - originRotate.value,
|
|
1434
|
+
focalX: focalX.value,
|
|
1435
|
+
focalY: focalY.value
|
|
1436
|
+
},
|
|
1437
|
+
canvas,
|
|
1438
|
+
intrinsic,
|
|
1439
|
+
config,
|
|
1440
|
+
true
|
|
1441
|
+
);
|
|
1442
|
+
runOnJS5(settleTo)(settled, settleMs);
|
|
1443
|
+
};
|
|
1444
|
+
const pan = Gesture5.Pan().minPointers(1).maxPointers(2).onStart((e) => {
|
|
1445
|
+
"worklet";
|
|
1446
|
+
startedPan.value = true;
|
|
1447
|
+
begin();
|
|
1448
|
+
rawPanX.value = e.translationX;
|
|
1449
|
+
rawPanY.value = e.translationY;
|
|
1450
|
+
originPanX.value = e.translationX;
|
|
1451
|
+
originPanY.value = e.translationY;
|
|
1452
|
+
}).onUpdate((e) => {
|
|
1453
|
+
"worklet";
|
|
1454
|
+
rawPanX.value = e.translationX;
|
|
1455
|
+
rawPanY.value = e.translationY;
|
|
1456
|
+
recompute();
|
|
1457
|
+
}).onFinalize(() => {
|
|
1458
|
+
"worklet";
|
|
1459
|
+
if (!startedPan.value) return;
|
|
1460
|
+
startedPan.value = false;
|
|
1461
|
+
finish();
|
|
1462
|
+
});
|
|
1463
|
+
const pinch = Gesture5.Pinch().onStart((e) => {
|
|
1464
|
+
"worklet";
|
|
1465
|
+
startedPinch.value = true;
|
|
1466
|
+
begin();
|
|
1467
|
+
rawPinch.value = e.scale;
|
|
1468
|
+
originPinch.value = e.scale;
|
|
1469
|
+
focalX.value = e.focalX / width;
|
|
1470
|
+
focalY.value = e.focalY / height;
|
|
1471
|
+
}).onUpdate((e) => {
|
|
1472
|
+
"worklet";
|
|
1473
|
+
rawPinch.value = e.scale;
|
|
1474
|
+
focalX.value = e.focalX / width;
|
|
1475
|
+
focalY.value = e.focalY / height;
|
|
1476
|
+
recompute();
|
|
1477
|
+
}).onFinalize(() => {
|
|
1478
|
+
"worklet";
|
|
1479
|
+
if (!startedPinch.value) return;
|
|
1480
|
+
startedPinch.value = false;
|
|
1481
|
+
finish();
|
|
1482
|
+
});
|
|
1483
|
+
const rotate = Gesture5.Rotation().enabled(config.rotationEnabled).onStart((e) => {
|
|
1484
|
+
"worklet";
|
|
1485
|
+
startedRotate.value = true;
|
|
1486
|
+
begin();
|
|
1487
|
+
rawRotate.value = e.rotation;
|
|
1488
|
+
originRotate.value = e.rotation;
|
|
1489
|
+
}).onUpdate((e) => {
|
|
1490
|
+
"worklet";
|
|
1491
|
+
rawRotate.value = e.rotation;
|
|
1492
|
+
recompute();
|
|
1493
|
+
}).onFinalize(() => {
|
|
1494
|
+
"worklet";
|
|
1495
|
+
if (!startedRotate.value) return;
|
|
1496
|
+
startedRotate.value = false;
|
|
1497
|
+
finish();
|
|
1498
|
+
});
|
|
1499
|
+
const doubleTap = Gesture5.Tap().numberOfTaps(2).onEnd((e) => {
|
|
1500
|
+
"worklet";
|
|
1501
|
+
runOnJS5(runDoubleTap)(e.x, e.y);
|
|
1502
|
+
});
|
|
1503
|
+
return Gesture5.Simultaneous(Gesture5.Race(pan, doubleTap), pinch, rotate);
|
|
1504
|
+
}, [metrics, canvas, intrinsic, config, settleTo, beginInteraction, runDoubleTap]);
|
|
1505
|
+
if (!layer) return null;
|
|
1506
|
+
return /* @__PURE__ */ jsx9(GestureDetector5, { gesture, children: /* @__PURE__ */ jsx9(Animated5.View, { style: StyleSheet6.absoluteFill }) });
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// src/gestures/routing.ts
|
|
1510
|
+
function resolveGestureSurface(activeTool) {
|
|
1511
|
+
switch (activeTool) {
|
|
1512
|
+
case "text":
|
|
1513
|
+
return "text";
|
|
1514
|
+
case "media":
|
|
1515
|
+
return "media";
|
|
1516
|
+
case "draw":
|
|
1517
|
+
return "draw";
|
|
1518
|
+
case "crop":
|
|
1519
|
+
return "crop";
|
|
1520
|
+
case "tilt":
|
|
1521
|
+
return "tilt";
|
|
1522
|
+
default:
|
|
1523
|
+
return "overlay";
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// src/gestures/InteractionGuides.tsx
|
|
1528
|
+
import { deleteTargetFromProfile as deleteTargetFromProfile2, useEditor as useEditor7 } from "@pictor-sdk/core";
|
|
1529
|
+
import { useCanvasMetrics as useCanvasMetrics6 } from "@pictor-sdk/skia";
|
|
1530
|
+
import { StyleSheet as StyleSheet7, Text, View as View2 } from "react-native";
|
|
1531
|
+
|
|
1532
|
+
// src/theme.ts
|
|
1533
|
+
import { createContext as createContext4, useContext as useContext4 } from "react";
|
|
1534
|
+
var defaultTheme = {
|
|
1535
|
+
colors: {
|
|
1536
|
+
background: "#000000",
|
|
1537
|
+
surface: "#121212",
|
|
1538
|
+
text: "#ffffff",
|
|
1539
|
+
textMuted: "#9a9a9a",
|
|
1540
|
+
accent: "#3897f0",
|
|
1541
|
+
danger: "#ed4956",
|
|
1542
|
+
track: "#3a3a3a",
|
|
1543
|
+
thumb: "#ffffff"
|
|
1544
|
+
},
|
|
1545
|
+
radii: { sm: 6, md: 10, lg: 16 },
|
|
1546
|
+
spacing: { xs: 4, sm: 8, md: 14, lg: 22 },
|
|
1547
|
+
font: { size: 15, sizeSm: 12, weight: "600" }
|
|
1548
|
+
};
|
|
1549
|
+
function mergeTheme(overrides) {
|
|
1550
|
+
if (!overrides) return defaultTheme;
|
|
1551
|
+
return {
|
|
1552
|
+
colors: { ...defaultTheme.colors, ...overrides.colors },
|
|
1553
|
+
radii: { ...defaultTheme.radii, ...overrides.radii },
|
|
1554
|
+
spacing: { ...defaultTheme.spacing, ...overrides.spacing },
|
|
1555
|
+
font: { ...defaultTheme.font, ...overrides.font }
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
var ThemeContext = createContext4(defaultTheme);
|
|
1559
|
+
function useTheme() {
|
|
1560
|
+
return useContext4(ThemeContext);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// src/gestures/InteractionGuides.tsx
|
|
1564
|
+
import { jsx as jsx10, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1565
|
+
function InteractionGuides({ deleteLabel = "Drag to delete" }) {
|
|
1566
|
+
const { profile } = useEditor7();
|
|
1567
|
+
const metrics = useCanvasMetrics6();
|
|
1568
|
+
const theme = useTheme();
|
|
1569
|
+
const { snap, layerDragActive, overDeleteTarget } = useInteractionState();
|
|
1570
|
+
const target = deleteTargetFromProfile2(profile.interaction?.layers);
|
|
1571
|
+
if (!metrics) return null;
|
|
1572
|
+
const radius = target ? target.radius * metrics.width : 0;
|
|
1573
|
+
const size = radius * 2;
|
|
1574
|
+
const active = overDeleteTarget;
|
|
1575
|
+
return /* @__PURE__ */ jsxs2(View2, { pointerEvents: "none", style: StyleSheet7.absoluteFill, children: [
|
|
1576
|
+
snap.centerX ? /* @__PURE__ */ jsx10(
|
|
1577
|
+
View2,
|
|
1578
|
+
{
|
|
1579
|
+
style: {
|
|
1580
|
+
position: "absolute",
|
|
1581
|
+
left: metrics.width / 2 - 1,
|
|
1582
|
+
top: 0,
|
|
1583
|
+
width: 2,
|
|
1584
|
+
height: metrics.height,
|
|
1585
|
+
backgroundColor: theme.colors.accent
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
) : null,
|
|
1589
|
+
snap.centerY ? /* @__PURE__ */ jsx10(
|
|
1590
|
+
View2,
|
|
1591
|
+
{
|
|
1592
|
+
style: {
|
|
1593
|
+
position: "absolute",
|
|
1594
|
+
top: metrics.height / 2 - 1,
|
|
1595
|
+
left: 0,
|
|
1596
|
+
height: 2,
|
|
1597
|
+
width: metrics.width,
|
|
1598
|
+
backgroundColor: theme.colors.accent
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
) : null,
|
|
1602
|
+
snap.rotation ? /* @__PURE__ */ jsx10(
|
|
1603
|
+
View2,
|
|
1604
|
+
{
|
|
1605
|
+
style: {
|
|
1606
|
+
position: "absolute",
|
|
1607
|
+
top: theme.spacing.md,
|
|
1608
|
+
alignSelf: "center",
|
|
1609
|
+
backgroundColor: "rgba(0,0,0,0.6)",
|
|
1610
|
+
borderRadius: theme.radii.lg,
|
|
1611
|
+
paddingHorizontal: theme.spacing.md,
|
|
1612
|
+
paddingVertical: theme.spacing.xs
|
|
1613
|
+
},
|
|
1614
|
+
children: /* @__PURE__ */ jsx10(
|
|
1615
|
+
Text,
|
|
1616
|
+
{
|
|
1617
|
+
style: {
|
|
1618
|
+
color: theme.colors.text,
|
|
1619
|
+
fontSize: theme.font.sizeSm,
|
|
1620
|
+
fontWeight: theme.font.weight
|
|
1621
|
+
},
|
|
1622
|
+
children: "Right-angle snap"
|
|
1623
|
+
}
|
|
1624
|
+
)
|
|
1625
|
+
}
|
|
1626
|
+
) : null,
|
|
1627
|
+
target && layerDragActive ? /* @__PURE__ */ jsx10(
|
|
1628
|
+
View2,
|
|
1629
|
+
{
|
|
1630
|
+
style: {
|
|
1631
|
+
position: "absolute",
|
|
1632
|
+
left: target.x * metrics.width - radius,
|
|
1633
|
+
top: target.y * metrics.height - radius,
|
|
1634
|
+
width: size,
|
|
1635
|
+
height: size,
|
|
1636
|
+
alignItems: "center",
|
|
1637
|
+
justifyContent: "center"
|
|
1638
|
+
},
|
|
1639
|
+
children: /* @__PURE__ */ jsx10(
|
|
1640
|
+
View2,
|
|
1641
|
+
{
|
|
1642
|
+
style: {
|
|
1643
|
+
width: size,
|
|
1644
|
+
height: size,
|
|
1645
|
+
borderRadius: radius,
|
|
1646
|
+
borderWidth: 2,
|
|
1647
|
+
borderColor: theme.colors.text,
|
|
1648
|
+
// a scrim keeps the target legible over light photos, which a
|
|
1649
|
+
// plain white ring is not
|
|
1650
|
+
backgroundColor: active ? theme.colors.danger : "rgba(0,0,0,0.45)",
|
|
1651
|
+
alignItems: "center",
|
|
1652
|
+
justifyContent: "center",
|
|
1653
|
+
transform: [{ scale: active ? 1.15 : 1 }]
|
|
1654
|
+
},
|
|
1655
|
+
children: /* @__PURE__ */ jsx10(
|
|
1656
|
+
Text,
|
|
1657
|
+
{
|
|
1658
|
+
style: {
|
|
1659
|
+
color: theme.colors.text,
|
|
1660
|
+
fontSize: radius * 0.7,
|
|
1661
|
+
fontWeight: theme.font.weight
|
|
1662
|
+
},
|
|
1663
|
+
children: "\xD7"
|
|
1664
|
+
}
|
|
1665
|
+
)
|
|
1666
|
+
}
|
|
1667
|
+
)
|
|
1668
|
+
}
|
|
1669
|
+
) : null,
|
|
1670
|
+
target && layerDragActive ? /* @__PURE__ */ jsx10(
|
|
1671
|
+
View2,
|
|
1672
|
+
{
|
|
1673
|
+
style: {
|
|
1674
|
+
position: "absolute",
|
|
1675
|
+
left: 0,
|
|
1676
|
+
right: 0,
|
|
1677
|
+
top: target.y * metrics.height - radius - theme.spacing.lg * 2,
|
|
1678
|
+
alignItems: "center"
|
|
1679
|
+
},
|
|
1680
|
+
children: /* @__PURE__ */ jsx10(
|
|
1681
|
+
Text,
|
|
1682
|
+
{
|
|
1683
|
+
style: {
|
|
1684
|
+
color: theme.colors.text,
|
|
1685
|
+
fontSize: theme.font.sizeSm,
|
|
1686
|
+
fontWeight: theme.font.weight,
|
|
1687
|
+
backgroundColor: "rgba(0,0,0,0.45)",
|
|
1688
|
+
paddingHorizontal: theme.spacing.sm,
|
|
1689
|
+
paddingVertical: theme.spacing.xs,
|
|
1690
|
+
borderRadius: theme.radii.sm,
|
|
1691
|
+
overflow: "hidden"
|
|
1692
|
+
},
|
|
1693
|
+
numberOfLines: 1,
|
|
1694
|
+
children: deleteLabel
|
|
1695
|
+
}
|
|
1696
|
+
)
|
|
1697
|
+
}
|
|
1698
|
+
) : null
|
|
1699
|
+
] });
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// src/ToolPanelHost.tsx
|
|
1703
|
+
import { useEditor as useEditor20, useTools } from "@pictor-sdk/core";
|
|
1704
|
+
import { useEffect as useEffect6, useRef as useRef11 } from "react";
|
|
1705
|
+
import { Pressable as Pressable14, Text as Text17, View as View17 } from "react-native";
|
|
1706
|
+
|
|
1707
|
+
// src/panels/AdjustPanel.tsx
|
|
1708
|
+
import {
|
|
1709
|
+
adjustmentResetPatch,
|
|
1710
|
+
hasAdjustment,
|
|
1711
|
+
useEditor as useEditor8,
|
|
1712
|
+
useEditorStore as useEditorStore3
|
|
1713
|
+
} from "@pictor-sdk/core";
|
|
1714
|
+
import { useState as useState4 } from "react";
|
|
1715
|
+
import { Pressable, ScrollView, Text as Text3, View as View4 } from "react-native";
|
|
1716
|
+
|
|
1717
|
+
// src/components/Slider.tsx
|
|
1718
|
+
import { useMemo as useMemo7, useRef as useRef9, useState as useState3 } from "react";
|
|
1719
|
+
import { Text as Text2, View as View3 } from "react-native";
|
|
1720
|
+
import { Gesture as Gesture6, GestureDetector as GestureDetector6 } from "react-native-gesture-handler";
|
|
1721
|
+
import Animated6 from "react-native-reanimated";
|
|
1722
|
+
import { runOnJS as runOnJS6 } from "react-native-worklets";
|
|
1723
|
+
import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1724
|
+
function applyDetent(value, centered, width = 0.05) {
|
|
1725
|
+
if (centered && Math.abs(value) < width) return 0;
|
|
1726
|
+
return value;
|
|
1727
|
+
}
|
|
1728
|
+
function Slider({
|
|
1729
|
+
value,
|
|
1730
|
+
min = -1,
|
|
1731
|
+
max = 1,
|
|
1732
|
+
centered = true,
|
|
1733
|
+
onStart,
|
|
1734
|
+
onChange,
|
|
1735
|
+
onEnd,
|
|
1736
|
+
testID,
|
|
1737
|
+
valueLabel,
|
|
1738
|
+
disabled = false
|
|
1739
|
+
}) {
|
|
1740
|
+
const theme = useTheme();
|
|
1741
|
+
const [width, setWidth] = useState3(0);
|
|
1742
|
+
const widthRef = useRef9(0);
|
|
1743
|
+
const startValue = useRef9(value);
|
|
1744
|
+
const valueRef = useRef9(value);
|
|
1745
|
+
valueRef.current = value;
|
|
1746
|
+
const onLayout = (e) => {
|
|
1747
|
+
setWidth(e.nativeEvent.layout.width);
|
|
1748
|
+
widthRef.current = e.nativeEvent.layout.width;
|
|
1749
|
+
};
|
|
1750
|
+
const handlers = useMemo7(() => {
|
|
1751
|
+
const begin = () => {
|
|
1752
|
+
startValue.current = valueRef.current;
|
|
1753
|
+
onStart?.();
|
|
1754
|
+
};
|
|
1755
|
+
const update = (tx) => {
|
|
1756
|
+
const w = widthRef.current;
|
|
1757
|
+
if (w === 0) return;
|
|
1758
|
+
const range = max - min;
|
|
1759
|
+
const raw = startValue.current + tx / w * range;
|
|
1760
|
+
const clamped = Math.min(max, Math.max(min, raw));
|
|
1761
|
+
onChange(applyDetent(clamped, centered && min < 0));
|
|
1762
|
+
};
|
|
1763
|
+
const finish = () => onEnd?.();
|
|
1764
|
+
return { begin, update, finish };
|
|
1765
|
+
}, [min, max, centered, onChange, onStart, onEnd]);
|
|
1766
|
+
const gesture = useMemo7(
|
|
1767
|
+
() => Gesture6.Pan().enabled(!disabled).onStart(() => {
|
|
1768
|
+
"worklet";
|
|
1769
|
+
runOnJS6(handlers.begin)();
|
|
1770
|
+
}).onUpdate((e) => {
|
|
1771
|
+
"worklet";
|
|
1772
|
+
runOnJS6(handlers.update)(e.translationX);
|
|
1773
|
+
}).onFinalize(() => {
|
|
1774
|
+
"worklet";
|
|
1775
|
+
runOnJS6(handlers.finish)();
|
|
1776
|
+
}),
|
|
1777
|
+
[handlers, disabled]
|
|
1778
|
+
);
|
|
1779
|
+
const t = (value - min) / (max - min);
|
|
1780
|
+
const thumbX = width * t;
|
|
1781
|
+
return /* @__PURE__ */ jsx11(GestureDetector6, { gesture, children: /* @__PURE__ */ jsxs3(
|
|
1782
|
+
Animated6.View,
|
|
1783
|
+
{
|
|
1784
|
+
testID,
|
|
1785
|
+
onLayout,
|
|
1786
|
+
style: {
|
|
1787
|
+
height: valueLabel ? 58 : 36,
|
|
1788
|
+
justifyContent: "flex-end",
|
|
1789
|
+
paddingBottom: 9,
|
|
1790
|
+
opacity: disabled ? 0.4 : 1
|
|
1791
|
+
},
|
|
1792
|
+
children: [
|
|
1793
|
+
valueLabel ? /* @__PURE__ */ jsx11(
|
|
1794
|
+
View3,
|
|
1795
|
+
{
|
|
1796
|
+
pointerEvents: "none",
|
|
1797
|
+
style: {
|
|
1798
|
+
position: "absolute",
|
|
1799
|
+
top: 0,
|
|
1800
|
+
left: Math.max(0, Math.min(width - 44, thumbX - 22)),
|
|
1801
|
+
width: 44,
|
|
1802
|
+
alignItems: "center",
|
|
1803
|
+
paddingVertical: 3,
|
|
1804
|
+
borderRadius: 10,
|
|
1805
|
+
backgroundColor: theme.colors.surface
|
|
1806
|
+
},
|
|
1807
|
+
children: /* @__PURE__ */ jsx11(Text2, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: valueLabel })
|
|
1808
|
+
}
|
|
1809
|
+
) : null,
|
|
1810
|
+
/* @__PURE__ */ jsx11(View3, { style: { height: 2, borderRadius: 1, backgroundColor: theme.colors.track } }),
|
|
1811
|
+
/* @__PURE__ */ jsx11(
|
|
1812
|
+
View3,
|
|
1813
|
+
{
|
|
1814
|
+
style: {
|
|
1815
|
+
position: "absolute",
|
|
1816
|
+
bottom: 9,
|
|
1817
|
+
left: centered && min < 0 ? Math.min(width / 2, thumbX) : 0,
|
|
1818
|
+
width: centered && min < 0 ? Math.abs(thumbX - width / 2) : thumbX,
|
|
1819
|
+
height: 2,
|
|
1820
|
+
borderRadius: 1,
|
|
1821
|
+
backgroundColor: theme.colors.accent
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
),
|
|
1825
|
+
centered && min < 0 ? /* @__PURE__ */ jsx11(
|
|
1826
|
+
View3,
|
|
1827
|
+
{
|
|
1828
|
+
style: {
|
|
1829
|
+
position: "absolute",
|
|
1830
|
+
left: width / 2 - 1,
|
|
1831
|
+
width: 2,
|
|
1832
|
+
height: 10,
|
|
1833
|
+
backgroundColor: theme.colors.textMuted
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
) : null,
|
|
1837
|
+
/* @__PURE__ */ jsx11(
|
|
1838
|
+
View3,
|
|
1839
|
+
{
|
|
1840
|
+
style: {
|
|
1841
|
+
position: "absolute",
|
|
1842
|
+
bottom: 1,
|
|
1843
|
+
left: Math.max(0, Math.min(width - 18, thumbX - 9)),
|
|
1844
|
+
width: 18,
|
|
1845
|
+
height: 18,
|
|
1846
|
+
borderRadius: 9,
|
|
1847
|
+
backgroundColor: theme.colors.thumb
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
)
|
|
1851
|
+
]
|
|
1852
|
+
}
|
|
1853
|
+
) });
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// src/panels/AdjustPanel.tsx
|
|
1857
|
+
import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1858
|
+
var LABELS = {
|
|
1859
|
+
brightness: "Brightness",
|
|
1860
|
+
contrast: "Contrast",
|
|
1861
|
+
saturation: "Saturation",
|
|
1862
|
+
warmth: "Warmth",
|
|
1863
|
+
tint: "Tint",
|
|
1864
|
+
exposure: "Exposure",
|
|
1865
|
+
highlights: "Highlights",
|
|
1866
|
+
shadows: "Shadows",
|
|
1867
|
+
fade: "Fade",
|
|
1868
|
+
structure: "Structure",
|
|
1869
|
+
sharpen: "Sharpen",
|
|
1870
|
+
grain: "Grain",
|
|
1871
|
+
vignette: "Vignette"
|
|
1872
|
+
};
|
|
1873
|
+
function AdjustPanel() {
|
|
1874
|
+
const { dispatch } = useEditor8();
|
|
1875
|
+
const store = useEditorStore3();
|
|
1876
|
+
const theme = useTheme();
|
|
1877
|
+
const layer = useTargetImageLayer();
|
|
1878
|
+
const keys = store.registry.adjustments;
|
|
1879
|
+
const [activeKey, setActiveKey] = useState4(keys[0] ?? "brightness");
|
|
1880
|
+
if (!layer) return null;
|
|
1881
|
+
const adjustments = layer.data.color.adjustments ?? {};
|
|
1882
|
+
const value = adjustments[activeKey] ?? 0;
|
|
1883
|
+
const touchedActive = hasAdjustment(adjustments, activeKey);
|
|
1884
|
+
const resetActive = () => dispatch({
|
|
1885
|
+
type: "layer/patchData",
|
|
1886
|
+
id: layer.id,
|
|
1887
|
+
patch: adjustmentResetPatch(activeKey)
|
|
1888
|
+
});
|
|
1889
|
+
return /* @__PURE__ */ jsxs4(View4, { style: { backgroundColor: theme.colors.background }, children: [
|
|
1890
|
+
/* @__PURE__ */ jsxs4(View4, { style: { paddingHorizontal: theme.spacing.lg }, children: [
|
|
1891
|
+
/* @__PURE__ */ jsxs4(
|
|
1892
|
+
View4,
|
|
1893
|
+
{
|
|
1894
|
+
style: {
|
|
1895
|
+
flexDirection: "row",
|
|
1896
|
+
alignItems: "center",
|
|
1897
|
+
justifyContent: "center",
|
|
1898
|
+
gap: theme.spacing.sm,
|
|
1899
|
+
minHeight: 28
|
|
1900
|
+
},
|
|
1901
|
+
children: [
|
|
1902
|
+
/* @__PURE__ */ jsxs4(
|
|
1903
|
+
Text3,
|
|
1904
|
+
{
|
|
1905
|
+
style: {
|
|
1906
|
+
color: theme.colors.textMuted,
|
|
1907
|
+
textAlign: "center",
|
|
1908
|
+
fontSize: theme.font.sizeSm
|
|
1909
|
+
},
|
|
1910
|
+
children: [
|
|
1911
|
+
LABELS[activeKey] ?? activeKey,
|
|
1912
|
+
" ",
|
|
1913
|
+
value === 0 ? "" : (value > 0 ? "+" : "") + Math.round(value * 100)
|
|
1914
|
+
]
|
|
1915
|
+
}
|
|
1916
|
+
),
|
|
1917
|
+
touchedActive ? /* @__PURE__ */ jsx12(
|
|
1918
|
+
Pressable,
|
|
1919
|
+
{
|
|
1920
|
+
accessibilityRole: "button",
|
|
1921
|
+
accessibilityLabel: `Reset ${LABELS[activeKey] ?? activeKey}`,
|
|
1922
|
+
onPress: resetActive,
|
|
1923
|
+
hitSlop: 8,
|
|
1924
|
+
style: {
|
|
1925
|
+
paddingHorizontal: theme.spacing.sm,
|
|
1926
|
+
paddingVertical: 2,
|
|
1927
|
+
borderRadius: theme.radii.sm,
|
|
1928
|
+
borderWidth: 1,
|
|
1929
|
+
borderColor: theme.colors.track
|
|
1930
|
+
},
|
|
1931
|
+
children: /* @__PURE__ */ jsx12(Text3, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: "Reset" })
|
|
1932
|
+
}
|
|
1933
|
+
) : null
|
|
1934
|
+
]
|
|
1935
|
+
}
|
|
1936
|
+
),
|
|
1937
|
+
/* @__PURE__ */ jsx12(
|
|
1938
|
+
Slider,
|
|
1939
|
+
{
|
|
1940
|
+
value,
|
|
1941
|
+
min: -1,
|
|
1942
|
+
max: 1,
|
|
1943
|
+
centered: true,
|
|
1944
|
+
valueLabel: `${value > 0 ? "+" : ""}${Math.round(value * 100)}`,
|
|
1945
|
+
onStart: () => dispatch({ type: "history/beginBatch", label: activeKey }),
|
|
1946
|
+
onChange: (v) => dispatch({
|
|
1947
|
+
type: "layer/patchData",
|
|
1948
|
+
id: layer.id,
|
|
1949
|
+
patch: { color: { adjustments: { [activeKey]: v } } }
|
|
1950
|
+
}),
|
|
1951
|
+
onEnd: () => dispatch({ type: "history/endBatch" })
|
|
1952
|
+
}
|
|
1953
|
+
)
|
|
1954
|
+
] }),
|
|
1955
|
+
/* @__PURE__ */ jsx12(
|
|
1956
|
+
ScrollView,
|
|
1957
|
+
{
|
|
1958
|
+
horizontal: true,
|
|
1959
|
+
showsHorizontalScrollIndicator: false,
|
|
1960
|
+
contentContainerStyle: { gap: theme.spacing.sm, padding: theme.spacing.md },
|
|
1961
|
+
style: { flexGrow: 0 },
|
|
1962
|
+
children: keys.map((key) => {
|
|
1963
|
+
const touched = (adjustments[key] ?? 0) !== 0;
|
|
1964
|
+
const active = key === activeKey;
|
|
1965
|
+
return /* @__PURE__ */ jsxs4(
|
|
1966
|
+
Pressable,
|
|
1967
|
+
{
|
|
1968
|
+
onPress: () => setActiveKey(key),
|
|
1969
|
+
style: { alignItems: "center", gap: 6, width: 76 },
|
|
1970
|
+
children: [
|
|
1971
|
+
/* @__PURE__ */ jsx12(View4, { style: { width: 48, height: 48, borderRadius: 24, borderWidth: 1, borderColor: active ? theme.colors.accent : theme.colors.textMuted, backgroundColor: active ? theme.colors.surface : "transparent", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx12(Text3, { style: { color: active ? theme.colors.accent : theme.colors.text, fontSize: 18 }, children: (LABELS[key] ?? key).slice(0, 1) }) }),
|
|
1972
|
+
/* @__PURE__ */ jsxs4(
|
|
1973
|
+
Text3,
|
|
1974
|
+
{
|
|
1975
|
+
style: {
|
|
1976
|
+
color: active ? theme.colors.accent : touched ? theme.colors.text : theme.colors.textMuted,
|
|
1977
|
+
fontSize: theme.font.sizeSm,
|
|
1978
|
+
fontWeight: theme.font.weight
|
|
1979
|
+
},
|
|
1980
|
+
children: [
|
|
1981
|
+
LABELS[key] ?? key,
|
|
1982
|
+
touched ? " \u2022" : ""
|
|
1983
|
+
]
|
|
1984
|
+
}
|
|
1985
|
+
)
|
|
1986
|
+
]
|
|
1987
|
+
},
|
|
1988
|
+
key
|
|
1989
|
+
);
|
|
1990
|
+
})
|
|
1991
|
+
}
|
|
1992
|
+
)
|
|
1993
|
+
] });
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// src/panels/AddPhotoPanel.tsx
|
|
1997
|
+
import { useEditor as useEditor9 } from "@pictor-sdk/core";
|
|
1998
|
+
import { useEffect as useEffect4, useMemo as useMemo8, useState as useState5 } from "react";
|
|
1999
|
+
import { Pressable as Pressable2, Text as Text4, View as View5 } from "react-native";
|
|
2000
|
+
|
|
2001
|
+
// src/panels/addPhotoController.ts
|
|
2002
|
+
import {
|
|
2003
|
+
createImageLayers,
|
|
2004
|
+
insertImageCommands
|
|
2005
|
+
} from "@pictor-sdk/core";
|
|
2006
|
+
function isInsertable(value) {
|
|
2007
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2008
|
+
const source = value.source;
|
|
2009
|
+
return source !== void 0 && source !== null && source !== "";
|
|
2010
|
+
}
|
|
2011
|
+
function toDenseImages(value) {
|
|
2012
|
+
if (!Array.isArray(value)) return null;
|
|
2013
|
+
const dense = [];
|
|
2014
|
+
for (let index = 0; index < value.length; index++) {
|
|
2015
|
+
if (!(index in value)) return null;
|
|
2016
|
+
const entry = value[index];
|
|
2017
|
+
if (!isInsertable(entry)) return null;
|
|
2018
|
+
dense.push(entry);
|
|
2019
|
+
}
|
|
2020
|
+
return dense;
|
|
2021
|
+
}
|
|
2022
|
+
function isDispatchable(command) {
|
|
2023
|
+
return typeof command === "object" && command !== null && typeof command.type === "string";
|
|
2024
|
+
}
|
|
2025
|
+
var InvalidImageResultError = class extends Error {
|
|
2026
|
+
constructor() {
|
|
2027
|
+
super("requestImages resolved with something that is not a list of images");
|
|
2028
|
+
this.name = "InvalidImageResultError";
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
function createAddPhotoController({
|
|
2032
|
+
requestImages,
|
|
2033
|
+
dispatch,
|
|
2034
|
+
select,
|
|
2035
|
+
onError,
|
|
2036
|
+
insert
|
|
2037
|
+
}) {
|
|
2038
|
+
let pending = false;
|
|
2039
|
+
let disposed = false;
|
|
2040
|
+
return {
|
|
2041
|
+
isPending: () => pending,
|
|
2042
|
+
isAvailable: () => typeof requestImages === "function",
|
|
2043
|
+
dispose: () => {
|
|
2044
|
+
disposed = true;
|
|
2045
|
+
},
|
|
2046
|
+
async request() {
|
|
2047
|
+
if (!requestImages || pending || disposed) return;
|
|
2048
|
+
pending = true;
|
|
2049
|
+
try {
|
|
2050
|
+
const result = await requestImages();
|
|
2051
|
+
if (disposed) return;
|
|
2052
|
+
if (result === null || result === void 0) return;
|
|
2053
|
+
const images = toDenseImages(result);
|
|
2054
|
+
if (!images) {
|
|
2055
|
+
onError?.(new InvalidImageResultError());
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (images.length === 0) return;
|
|
2059
|
+
const layers = createImageLayers(images, insert);
|
|
2060
|
+
const commands = insertImageCommands(layers, insert);
|
|
2061
|
+
if (!commands.every(isDispatchable)) {
|
|
2062
|
+
onError?.(new InvalidImageResultError());
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
for (const command of commands) dispatch(command);
|
|
2066
|
+
const last = layers[layers.length - 1];
|
|
2067
|
+
if (last) select([last.id]);
|
|
2068
|
+
} catch (error) {
|
|
2069
|
+
if (!disposed) onError?.(error);
|
|
2070
|
+
} finally {
|
|
2071
|
+
pending = false;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/panels/AddPhotoPanel.tsx
|
|
2078
|
+
import { jsx as jsx13, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2079
|
+
function AddPhotoPanel() {
|
|
2080
|
+
const { dispatch, select } = useEditor9();
|
|
2081
|
+
const host = useHostCallbacks();
|
|
2082
|
+
const theme = useTheme();
|
|
2083
|
+
const [pending, setPending] = useState5(false);
|
|
2084
|
+
const controller = useMemo8(
|
|
2085
|
+
() => createAddPhotoController({
|
|
2086
|
+
// Read through the ref at call time, so a host re-creating its closure
|
|
2087
|
+
// does not rebuild the controller mid-request.
|
|
2088
|
+
requestImages: host.current.requestImages ? () => host.current.requestImages() : void 0,
|
|
2089
|
+
dispatch,
|
|
2090
|
+
select,
|
|
2091
|
+
onError: (error) => host.current.onError?.(error)
|
|
2092
|
+
}),
|
|
2093
|
+
[dispatch, select, host]
|
|
2094
|
+
);
|
|
2095
|
+
useEffect4(() => () => controller.dispose(), [controller]);
|
|
2096
|
+
const available = controller.isAvailable();
|
|
2097
|
+
const press = async () => {
|
|
2098
|
+
if (!available || pending) return;
|
|
2099
|
+
setPending(true);
|
|
2100
|
+
try {
|
|
2101
|
+
await controller.request();
|
|
2102
|
+
} finally {
|
|
2103
|
+
setPending(false);
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
return /* @__PURE__ */ jsxs5(View5, { style: { backgroundColor: theme.colors.background, padding: theme.spacing.lg }, children: [
|
|
2107
|
+
/* @__PURE__ */ jsx13(
|
|
2108
|
+
Pressable2,
|
|
2109
|
+
{
|
|
2110
|
+
accessibilityRole: "button",
|
|
2111
|
+
accessibilityLabel: "Add photos",
|
|
2112
|
+
accessibilityState: { disabled: !available || pending },
|
|
2113
|
+
disabled: !available || pending,
|
|
2114
|
+
onPress: press,
|
|
2115
|
+
style: {
|
|
2116
|
+
paddingVertical: theme.spacing.md,
|
|
2117
|
+
borderRadius: theme.radii.sm,
|
|
2118
|
+
borderWidth: 1,
|
|
2119
|
+
borderColor: available ? theme.colors.accent : theme.colors.track,
|
|
2120
|
+
alignItems: "center",
|
|
2121
|
+
opacity: available && !pending ? 1 : 0.45
|
|
2122
|
+
},
|
|
2123
|
+
children: /* @__PURE__ */ jsx13(
|
|
2124
|
+
Text4,
|
|
2125
|
+
{
|
|
2126
|
+
style: {
|
|
2127
|
+
color: available ? theme.colors.accent : theme.colors.textMuted,
|
|
2128
|
+
fontSize: theme.font.size,
|
|
2129
|
+
fontWeight: theme.font.weight
|
|
2130
|
+
},
|
|
2131
|
+
children: pending ? "Choosing\u2026" : "Add photos"
|
|
2132
|
+
}
|
|
2133
|
+
)
|
|
2134
|
+
}
|
|
2135
|
+
),
|
|
2136
|
+
/* @__PURE__ */ jsx13(
|
|
2137
|
+
Text4,
|
|
2138
|
+
{
|
|
2139
|
+
style: {
|
|
2140
|
+
color: theme.colors.textMuted,
|
|
2141
|
+
fontSize: theme.font.sizeSm,
|
|
2142
|
+
textAlign: "center",
|
|
2143
|
+
marginTop: theme.spacing.sm
|
|
2144
|
+
},
|
|
2145
|
+
children: available ? "Pick one or more photos to place on the story" : `Unavailable \u2014 pass \`requestImages\` to <Editor>`
|
|
2146
|
+
}
|
|
2147
|
+
)
|
|
2148
|
+
] });
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
// src/panels/ColorPanel.tsx
|
|
2152
|
+
import {
|
|
2153
|
+
isInertTint,
|
|
2154
|
+
tintPatch,
|
|
2155
|
+
useEditor as useEditor10
|
|
2156
|
+
} from "@pictor-sdk/core";
|
|
2157
|
+
import { useState as useState6 } from "react";
|
|
2158
|
+
import { Pressable as Pressable3, ScrollView as ScrollView2, Text as Text5, View as View6 } from "react-native";
|
|
2159
|
+
import { jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2160
|
+
var TINT_SWATCHES = [
|
|
2161
|
+
"#1f8a8a",
|
|
2162
|
+
"#2f6fd0",
|
|
2163
|
+
"#7a5cd0",
|
|
2164
|
+
"#c04a9e",
|
|
2165
|
+
"#c8443c",
|
|
2166
|
+
"#d99038",
|
|
2167
|
+
"#8a9a3f",
|
|
2168
|
+
"#3f9a6a"
|
|
2169
|
+
];
|
|
2170
|
+
var ENDS = [
|
|
2171
|
+
{ key: "shadows", label: "Shadows" },
|
|
2172
|
+
{ key: "highlights", label: "Highlights" }
|
|
2173
|
+
];
|
|
2174
|
+
var INITIAL_STRENGTH = 0.5;
|
|
2175
|
+
function ColorPanel() {
|
|
2176
|
+
const { dispatch } = useEditor10();
|
|
2177
|
+
const theme = useTheme();
|
|
2178
|
+
const layer = useTargetImageLayer();
|
|
2179
|
+
const [end, setEnd] = useState6("shadows");
|
|
2180
|
+
if (!layer) return null;
|
|
2181
|
+
const data = layer.data;
|
|
2182
|
+
const tint = data.color.tinting?.[end];
|
|
2183
|
+
const active = isInertTint(tint) ? void 0 : tint;
|
|
2184
|
+
const patch = (next) => dispatch({
|
|
2185
|
+
type: "layer/patchData",
|
|
2186
|
+
id: layer.id,
|
|
2187
|
+
patch: { color: tintPatch(end, next) }
|
|
2188
|
+
});
|
|
2189
|
+
const pick = (color) => patch(
|
|
2190
|
+
active?.color === color ? void 0 : { color, strength: active?.strength ?? INITIAL_STRENGTH }
|
|
2191
|
+
);
|
|
2192
|
+
return /* @__PURE__ */ jsxs6(View6, { style: { backgroundColor: theme.colors.background, paddingBottom: theme.spacing.md }, children: [
|
|
2193
|
+
/* @__PURE__ */ jsx14(
|
|
2194
|
+
View6,
|
|
2195
|
+
{
|
|
2196
|
+
style: {
|
|
2197
|
+
flexDirection: "row",
|
|
2198
|
+
justifyContent: "center",
|
|
2199
|
+
gap: theme.spacing.md,
|
|
2200
|
+
paddingTop: theme.spacing.sm
|
|
2201
|
+
},
|
|
2202
|
+
children: ENDS.map(({ key, label }) => {
|
|
2203
|
+
const selected = key === end;
|
|
2204
|
+
const set = !isInertTint(data.color.tinting?.[key]);
|
|
2205
|
+
return /* @__PURE__ */ jsx14(
|
|
2206
|
+
Pressable3,
|
|
2207
|
+
{
|
|
2208
|
+
accessibilityRole: "button",
|
|
2209
|
+
accessibilityLabel: `Tint ${label.toLowerCase()}`,
|
|
2210
|
+
onPress: () => setEnd(key),
|
|
2211
|
+
style: {
|
|
2212
|
+
paddingHorizontal: theme.spacing.md,
|
|
2213
|
+
paddingVertical: 6,
|
|
2214
|
+
borderRadius: theme.radii.sm,
|
|
2215
|
+
borderWidth: 1,
|
|
2216
|
+
borderColor: selected ? theme.colors.accent : "transparent"
|
|
2217
|
+
},
|
|
2218
|
+
children: /* @__PURE__ */ jsxs6(
|
|
2219
|
+
Text5,
|
|
2220
|
+
{
|
|
2221
|
+
style: {
|
|
2222
|
+
color: selected ? theme.colors.accent : theme.colors.textMuted,
|
|
2223
|
+
fontSize: theme.font.sizeSm,
|
|
2224
|
+
fontWeight: theme.font.weight
|
|
2225
|
+
},
|
|
2226
|
+
children: [
|
|
2227
|
+
label,
|
|
2228
|
+
set ? " \u2022" : ""
|
|
2229
|
+
]
|
|
2230
|
+
}
|
|
2231
|
+
)
|
|
2232
|
+
},
|
|
2233
|
+
key
|
|
2234
|
+
);
|
|
2235
|
+
})
|
|
2236
|
+
}
|
|
2237
|
+
),
|
|
2238
|
+
/* @__PURE__ */ jsx14(
|
|
2239
|
+
ScrollView2,
|
|
2240
|
+
{
|
|
2241
|
+
horizontal: true,
|
|
2242
|
+
showsHorizontalScrollIndicator: false,
|
|
2243
|
+
contentContainerStyle: { gap: theme.spacing.sm, paddingHorizontal: theme.spacing.lg, paddingVertical: theme.spacing.md },
|
|
2244
|
+
style: { flexGrow: 0 },
|
|
2245
|
+
children: TINT_SWATCHES.map((color) => /* @__PURE__ */ jsx14(
|
|
2246
|
+
Pressable3,
|
|
2247
|
+
{
|
|
2248
|
+
accessibilityRole: "button",
|
|
2249
|
+
accessibilityLabel: `Tint ${end} ${color}`,
|
|
2250
|
+
onPress: () => pick(color),
|
|
2251
|
+
style: {
|
|
2252
|
+
width: 40,
|
|
2253
|
+
height: 40,
|
|
2254
|
+
borderRadius: 20,
|
|
2255
|
+
backgroundColor: color,
|
|
2256
|
+
borderWidth: active?.color === color ? 3 : 1,
|
|
2257
|
+
borderColor: active?.color === color ? theme.colors.text : theme.colors.track
|
|
2258
|
+
}
|
|
2259
|
+
},
|
|
2260
|
+
color
|
|
2261
|
+
))
|
|
2262
|
+
}
|
|
2263
|
+
),
|
|
2264
|
+
/* @__PURE__ */ jsxs6(View6, { style: { paddingHorizontal: theme.spacing.lg }, children: [
|
|
2265
|
+
/* @__PURE__ */ jsxs6(
|
|
2266
|
+
View6,
|
|
2267
|
+
{
|
|
2268
|
+
style: {
|
|
2269
|
+
flexDirection: "row",
|
|
2270
|
+
alignItems: "center",
|
|
2271
|
+
justifyContent: "center",
|
|
2272
|
+
gap: theme.spacing.sm,
|
|
2273
|
+
minHeight: 28
|
|
2274
|
+
},
|
|
2275
|
+
children: [
|
|
2276
|
+
/* @__PURE__ */ jsx14(Text5, { style: { color: theme.colors.textMuted, fontSize: theme.font.sizeSm }, children: active ? `Strength ${Math.round(active.strength * 100)}` : "Pick a colour" }),
|
|
2277
|
+
active ? /* @__PURE__ */ jsx14(
|
|
2278
|
+
Pressable3,
|
|
2279
|
+
{
|
|
2280
|
+
accessibilityRole: "button",
|
|
2281
|
+
accessibilityLabel: `Reset ${end} tint`,
|
|
2282
|
+
onPress: () => patch(void 0),
|
|
2283
|
+
hitSlop: 8,
|
|
2284
|
+
style: {
|
|
2285
|
+
paddingHorizontal: theme.spacing.sm,
|
|
2286
|
+
paddingVertical: 2,
|
|
2287
|
+
borderRadius: theme.radii.sm,
|
|
2288
|
+
borderWidth: 1,
|
|
2289
|
+
borderColor: theme.colors.track
|
|
2290
|
+
},
|
|
2291
|
+
children: /* @__PURE__ */ jsx14(Text5, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: "Reset" })
|
|
2292
|
+
}
|
|
2293
|
+
) : null
|
|
2294
|
+
]
|
|
2295
|
+
}
|
|
2296
|
+
),
|
|
2297
|
+
/* @__PURE__ */ jsx14(
|
|
2298
|
+
Slider,
|
|
2299
|
+
{
|
|
2300
|
+
value: active?.strength ?? 0,
|
|
2301
|
+
min: 0,
|
|
2302
|
+
max: 1,
|
|
2303
|
+
centered: false,
|
|
2304
|
+
testID: "tint-strength",
|
|
2305
|
+
disabled: !active,
|
|
2306
|
+
valueLabel: `${Math.round((active?.strength ?? 0) * 100)}`,
|
|
2307
|
+
onStart: () => dispatch({ type: "history/beginBatch", label: `tint-${end}` }),
|
|
2308
|
+
onChange: (strength) => {
|
|
2309
|
+
if (!active) return;
|
|
2310
|
+
patch({ color: active.color, strength });
|
|
2311
|
+
},
|
|
2312
|
+
onEnd: () => dispatch({ type: "history/endBatch" })
|
|
2313
|
+
}
|
|
2314
|
+
)
|
|
2315
|
+
] })
|
|
2316
|
+
] });
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// src/panels/CropPanel.tsx
|
|
2320
|
+
import {
|
|
2321
|
+
cropFitScale as cropFitScale2,
|
|
2322
|
+
flipPatch,
|
|
2323
|
+
isFlipped,
|
|
2324
|
+
useEditor as useEditor11,
|
|
2325
|
+
useEditorStore as useEditorStore4
|
|
2326
|
+
} from "@pictor-sdk/core";
|
|
2327
|
+
import { Pressable as Pressable4, ScrollView as ScrollView3, Text as Text7, View as View8 } from "react-native";
|
|
2328
|
+
|
|
2329
|
+
// src/components/StraightenRuler.tsx
|
|
2330
|
+
import { useMemo as useMemo9, useRef as useRef10 } from "react";
|
|
2331
|
+
import { Text as Text6, View as View7 } from "react-native";
|
|
2332
|
+
import { Gesture as Gesture7, GestureDetector as GestureDetector7 } from "react-native-gesture-handler";
|
|
2333
|
+
import Animated7 from "react-native-reanimated";
|
|
2334
|
+
import { runOnJS as runOnJS7 } from "react-native-worklets";
|
|
2335
|
+
import { jsx as jsx15, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2336
|
+
var PX_PER_DEGREE = 60;
|
|
2337
|
+
function StraightenRuler({ value, min = -45, max = 45, onStart, onChange, onEnd }) {
|
|
2338
|
+
const theme = useTheme();
|
|
2339
|
+
const start = useRef10(value);
|
|
2340
|
+
const current = useRef10(value);
|
|
2341
|
+
current.current = value;
|
|
2342
|
+
const handlers = useMemo9(() => ({
|
|
2343
|
+
begin: () => {
|
|
2344
|
+
start.current = current.current;
|
|
2345
|
+
onStart?.();
|
|
2346
|
+
},
|
|
2347
|
+
update: (translationX) => {
|
|
2348
|
+
const raw = Math.min(max, Math.max(min, start.current - translationX / PX_PER_DEGREE));
|
|
2349
|
+
onChange(Math.abs(raw) < 0.15 ? 0 : raw);
|
|
2350
|
+
},
|
|
2351
|
+
finish: () => onEnd?.()
|
|
2352
|
+
}), [min, max, onStart, onChange, onEnd]);
|
|
2353
|
+
const gesture = useMemo9(() => Gesture7.Pan().onStart(() => {
|
|
2354
|
+
"worklet";
|
|
2355
|
+
runOnJS7(handlers.begin)();
|
|
2356
|
+
}).onUpdate((event) => {
|
|
2357
|
+
"worklet";
|
|
2358
|
+
runOnJS7(handlers.update)(event.translationX);
|
|
2359
|
+
}).onFinalize(() => {
|
|
2360
|
+
"worklet";
|
|
2361
|
+
runOnJS7(handlers.finish)();
|
|
2362
|
+
}), [handlers]);
|
|
2363
|
+
const rounded = Math.round(value * 10) / 10;
|
|
2364
|
+
return /* @__PURE__ */ jsx15(GestureDetector7, { gesture, children: /* @__PURE__ */ jsxs7(Animated7.View, { style: { height: 76, overflow: "hidden", justifyContent: "flex-end" }, children: [
|
|
2365
|
+
/* @__PURE__ */ jsx15(View7, { style: { position: "absolute", top: 0, alignSelf: "center", paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12, backgroundColor: theme.colors.surface }, children: /* @__PURE__ */ jsxs7(Text6, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: [
|
|
2366
|
+
rounded.toFixed(1),
|
|
2367
|
+
"\xB0"
|
|
2368
|
+
] }) }),
|
|
2369
|
+
/* @__PURE__ */ jsx15(View7, { style: { height: 34, flexDirection: "row", justifyContent: "center", alignItems: "flex-end", gap: 8 }, children: Array.from({ length: 31 }, (_, index) => {
|
|
2370
|
+
const offset = index - 15;
|
|
2371
|
+
const major = offset % 5 === 0;
|
|
2372
|
+
return /* @__PURE__ */ jsx15(View7, { style: { width: 1, height: major ? 22 : 11, backgroundColor: offset === 0 ? theme.colors.accent : theme.colors.textMuted } }, offset);
|
|
2373
|
+
}) }),
|
|
2374
|
+
/* @__PURE__ */ jsx15(View7, { style: { position: "absolute", bottom: 0, left: "50%", width: 2, height: 30, backgroundColor: theme.colors.accent } })
|
|
2375
|
+
] }) });
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
// src/panels/cropMath.ts
|
|
2379
|
+
function canvasSizeForAspect(current, aspect) {
|
|
2380
|
+
if (aspect === "free") return current;
|
|
2381
|
+
return { width: current.width, height: Math.round(current.width / aspect) };
|
|
2382
|
+
}
|
|
2383
|
+
function centeredCropForAspect(intrinsic, aspect) {
|
|
2384
|
+
const sourceAspect = intrinsic.width / intrinsic.height;
|
|
2385
|
+
if (sourceAspect > aspect) {
|
|
2386
|
+
const width = aspect / sourceAspect;
|
|
2387
|
+
return { x: (1 - width) / 2, y: 0, width, height: 1 };
|
|
2388
|
+
}
|
|
2389
|
+
const height = sourceAspect / aspect;
|
|
2390
|
+
return { x: 0, y: (1 - height) / 2, width: 1, height };
|
|
2391
|
+
}
|
|
2392
|
+
var STRAIGHTEN_LIMIT = 45 * Math.PI / 180;
|
|
2393
|
+
function straightenToRadians(value) {
|
|
2394
|
+
return Math.max(-1, Math.min(1, value)) * STRAIGHTEN_LIMIT;
|
|
2395
|
+
}
|
|
2396
|
+
function radiansToStraighten(rad) {
|
|
2397
|
+
return Math.max(-1, Math.min(1, rad / STRAIGHTEN_LIMIT));
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
// src/panels/CropPanel.tsx
|
|
2401
|
+
import { jsx as jsx16, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2402
|
+
function aspectLabel(aspect) {
|
|
2403
|
+
if (aspect === "free") return "Free";
|
|
2404
|
+
const known = [
|
|
2405
|
+
[4 / 5, "4:5"],
|
|
2406
|
+
[1, "1:1"],
|
|
2407
|
+
[16 / 9, "16:9"],
|
|
2408
|
+
[9 / 16, "9:16"],
|
|
2409
|
+
[3 / 4, "3:4"],
|
|
2410
|
+
[4 / 3, "4:3"]
|
|
2411
|
+
];
|
|
2412
|
+
for (const [value, label] of known) {
|
|
2413
|
+
if (Math.abs(aspect - value) < 1e-6) return label;
|
|
2414
|
+
}
|
|
2415
|
+
return aspect.toFixed(2);
|
|
2416
|
+
}
|
|
2417
|
+
function CropPanel() {
|
|
2418
|
+
const { document, dispatch, profile } = useEditor11();
|
|
2419
|
+
useEditorStore4();
|
|
2420
|
+
const theme = useTheme();
|
|
2421
|
+
const layer = useTargetImageLayer();
|
|
2422
|
+
if (!layer) return null;
|
|
2423
|
+
const data = layer.data;
|
|
2424
|
+
const straighten = data.straighten ?? 0;
|
|
2425
|
+
const applyAspect = (aspect) => {
|
|
2426
|
+
const nextSize = canvasSizeForAspect(document.size, aspect);
|
|
2427
|
+
dispatch({ type: "history/beginBatch", label: "aspect" });
|
|
2428
|
+
dispatch({ type: "doc/patch", patch: { size: nextSize } });
|
|
2429
|
+
dispatch({ type: "layer/patchData", id: layer.id, patch: { crop: void 0 } });
|
|
2430
|
+
dispatch({
|
|
2431
|
+
type: "layer/transform",
|
|
2432
|
+
id: layer.id,
|
|
2433
|
+
patch: {
|
|
2434
|
+
x: 0.5,
|
|
2435
|
+
y: 0.5,
|
|
2436
|
+
scale: cropFitScale2(data.intrinsic, nextSize, profile.interaction?.crop?.fit ?? "contain")
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
2439
|
+
dispatch({ type: "history/endBatch" });
|
|
2440
|
+
};
|
|
2441
|
+
const rotate90 = () => {
|
|
2442
|
+
const current = layer.transform.rotation;
|
|
2443
|
+
const rotation = current + Math.PI / 2;
|
|
2444
|
+
dispatch({
|
|
2445
|
+
type: "layer/transform",
|
|
2446
|
+
id: layer.id,
|
|
2447
|
+
patch: {
|
|
2448
|
+
rotation,
|
|
2449
|
+
x: 0.5,
|
|
2450
|
+
y: 0.5,
|
|
2451
|
+
scale: cropFitScale2(data.intrinsic, document.size, profile.interaction?.crop?.fit ?? "contain", rotation)
|
|
2452
|
+
}
|
|
2453
|
+
});
|
|
2454
|
+
};
|
|
2455
|
+
const flip = (axis) => dispatch({
|
|
2456
|
+
type: "layer/transform",
|
|
2457
|
+
id: layer.id,
|
|
2458
|
+
patch: flipPatch(layer.transform, axis)
|
|
2459
|
+
});
|
|
2460
|
+
return /* @__PURE__ */ jsxs8(View8, { style: { backgroundColor: theme.colors.background }, children: [
|
|
2461
|
+
/* @__PURE__ */ jsx16(View8, { style: { paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ jsx16(
|
|
2462
|
+
StraightenRuler,
|
|
2463
|
+
{
|
|
2464
|
+
value: straighten * 180 / Math.PI,
|
|
2465
|
+
min: -45,
|
|
2466
|
+
max: 45,
|
|
2467
|
+
onStart: () => dispatch({ type: "history/beginBatch", label: "straighten" }),
|
|
2468
|
+
onChange: (v) => dispatch({
|
|
2469
|
+
type: "layer/patchData",
|
|
2470
|
+
id: layer.id,
|
|
2471
|
+
patch: { straighten: v * Math.PI / 180 }
|
|
2472
|
+
}),
|
|
2473
|
+
onEnd: () => dispatch({ type: "history/endBatch" })
|
|
2474
|
+
}
|
|
2475
|
+
) }),
|
|
2476
|
+
/* @__PURE__ */ jsxs8(
|
|
2477
|
+
ScrollView3,
|
|
2478
|
+
{
|
|
2479
|
+
horizontal: true,
|
|
2480
|
+
showsHorizontalScrollIndicator: false,
|
|
2481
|
+
contentContainerStyle: { gap: theme.spacing.sm, padding: theme.spacing.md },
|
|
2482
|
+
style: { flexGrow: 0 },
|
|
2483
|
+
children: [
|
|
2484
|
+
/* @__PURE__ */ jsx16(
|
|
2485
|
+
Pressable4,
|
|
2486
|
+
{
|
|
2487
|
+
accessibilityRole: "button",
|
|
2488
|
+
accessibilityLabel: "Rotate 90 degrees",
|
|
2489
|
+
onPress: rotate90,
|
|
2490
|
+
style: {
|
|
2491
|
+
paddingHorizontal: theme.spacing.md,
|
|
2492
|
+
paddingVertical: theme.spacing.sm,
|
|
2493
|
+
borderRadius: theme.radii.md,
|
|
2494
|
+
backgroundColor: theme.colors.surface
|
|
2495
|
+
},
|
|
2496
|
+
children: /* @__PURE__ */ jsx16(Text7, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm, fontWeight: theme.font.weight }, children: "\u27F3 90\xB0" })
|
|
2497
|
+
}
|
|
2498
|
+
),
|
|
2499
|
+
["horizontal", "vertical"].map((axis) => {
|
|
2500
|
+
const on = isFlipped(layer.transform, axis);
|
|
2501
|
+
return /* @__PURE__ */ jsx16(
|
|
2502
|
+
Pressable4,
|
|
2503
|
+
{
|
|
2504
|
+
accessibilityRole: "button",
|
|
2505
|
+
accessibilityLabel: `Flip ${axis}`,
|
|
2506
|
+
accessibilityState: { selected: on },
|
|
2507
|
+
onPress: () => flip(axis),
|
|
2508
|
+
style: {
|
|
2509
|
+
paddingHorizontal: theme.spacing.md,
|
|
2510
|
+
paddingVertical: theme.spacing.sm,
|
|
2511
|
+
borderRadius: theme.radii.md,
|
|
2512
|
+
backgroundColor: theme.colors.surface,
|
|
2513
|
+
borderWidth: 1,
|
|
2514
|
+
borderColor: on ? theme.colors.accent : "transparent"
|
|
2515
|
+
},
|
|
2516
|
+
children: /* @__PURE__ */ jsx16(
|
|
2517
|
+
Text7,
|
|
2518
|
+
{
|
|
2519
|
+
style: {
|
|
2520
|
+
color: on ? theme.colors.accent : theme.colors.text,
|
|
2521
|
+
fontSize: theme.font.sizeSm,
|
|
2522
|
+
fontWeight: theme.font.weight
|
|
2523
|
+
},
|
|
2524
|
+
children: axis === "horizontal" ? "\u21CB" : "\u21C5"
|
|
2525
|
+
}
|
|
2526
|
+
)
|
|
2527
|
+
},
|
|
2528
|
+
axis
|
|
2529
|
+
);
|
|
2530
|
+
}),
|
|
2531
|
+
profile.aspects.map((aspect, i) => /* @__PURE__ */ jsx16(
|
|
2532
|
+
Pressable4,
|
|
2533
|
+
{
|
|
2534
|
+
onPress: () => applyAspect(aspect),
|
|
2535
|
+
style: {
|
|
2536
|
+
width: 58,
|
|
2537
|
+
height: 48,
|
|
2538
|
+
borderRadius: theme.radii.md,
|
|
2539
|
+
borderWidth: 1,
|
|
2540
|
+
borderColor: theme.colors.textMuted,
|
|
2541
|
+
backgroundColor: "transparent",
|
|
2542
|
+
alignItems: "center",
|
|
2543
|
+
justifyContent: "center"
|
|
2544
|
+
},
|
|
2545
|
+
children: /* @__PURE__ */ jsx16(Text7, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm, fontWeight: theme.font.weight }, children: aspectLabel(aspect) })
|
|
2546
|
+
},
|
|
2547
|
+
i
|
|
2548
|
+
))
|
|
2549
|
+
]
|
|
2550
|
+
}
|
|
2551
|
+
)
|
|
2552
|
+
] });
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
// src/panels/DrawPanel.tsx
|
|
2556
|
+
import { Pressable as Pressable5, ScrollView as ScrollView4, Text as Text8, View as View9 } from "react-native";
|
|
2557
|
+
import { jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2558
|
+
var BRUSHES = [
|
|
2559
|
+
{ id: "pen", label: "Pen" },
|
|
2560
|
+
{ id: "marker", label: "Marker" },
|
|
2561
|
+
{ id: "neon", label: "Neon" },
|
|
2562
|
+
{ id: "highlighter", label: "Highlight" },
|
|
2563
|
+
{ id: "eraser", label: "Eraser" }
|
|
2564
|
+
];
|
|
2565
|
+
var SWATCHES = [
|
|
2566
|
+
"#ffffff",
|
|
2567
|
+
"#000000",
|
|
2568
|
+
"#ed4956",
|
|
2569
|
+
"#ffb43b",
|
|
2570
|
+
"#ffe25c",
|
|
2571
|
+
"#78de85",
|
|
2572
|
+
"#4dc4ff",
|
|
2573
|
+
"#3897f0",
|
|
2574
|
+
"#a15fed",
|
|
2575
|
+
"#d977b1"
|
|
2576
|
+
];
|
|
2577
|
+
function DrawPanel() {
|
|
2578
|
+
const theme = useTheme();
|
|
2579
|
+
const [brush, setBrush] = useUiState("draw.brush", DEFAULT_BRUSH);
|
|
2580
|
+
const [eyedropper, setEyedropper] = useUiState("draw.eyedropper", false);
|
|
2581
|
+
return /* @__PURE__ */ jsxs9(View9, { style: { backgroundColor: theme.colors.background }, children: [
|
|
2582
|
+
/* @__PURE__ */ jsx17(View9, { style: { paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ jsx17(
|
|
2583
|
+
Slider,
|
|
2584
|
+
{
|
|
2585
|
+
value: brush.width,
|
|
2586
|
+
min: 2e-3,
|
|
2587
|
+
max: 0.06,
|
|
2588
|
+
centered: false,
|
|
2589
|
+
onChange: (width) => setBrush({ ...brush, width })
|
|
2590
|
+
}
|
|
2591
|
+
) }),
|
|
2592
|
+
/* @__PURE__ */ jsxs9(
|
|
2593
|
+
ScrollView4,
|
|
2594
|
+
{
|
|
2595
|
+
horizontal: true,
|
|
2596
|
+
showsHorizontalScrollIndicator: false,
|
|
2597
|
+
contentContainerStyle: { gap: theme.spacing.sm, paddingHorizontal: theme.spacing.md, alignItems: "center" },
|
|
2598
|
+
style: { flexGrow: 0 },
|
|
2599
|
+
children: [
|
|
2600
|
+
/* @__PURE__ */ jsx17(
|
|
2601
|
+
Pressable5,
|
|
2602
|
+
{
|
|
2603
|
+
onPress: () => setEyedropper(!eyedropper),
|
|
2604
|
+
style: {
|
|
2605
|
+
width: 28,
|
|
2606
|
+
height: 28,
|
|
2607
|
+
borderRadius: 14,
|
|
2608
|
+
borderWidth: 2,
|
|
2609
|
+
borderColor: eyedropper ? theme.colors.accent : theme.colors.textMuted,
|
|
2610
|
+
alignItems: "center",
|
|
2611
|
+
justifyContent: "center"
|
|
2612
|
+
},
|
|
2613
|
+
children: /* @__PURE__ */ jsx17(Text8, { style: { color: theme.colors.text, fontSize: 13 }, children: "\u{1F4A7}" })
|
|
2614
|
+
}
|
|
2615
|
+
),
|
|
2616
|
+
SWATCHES.map((color) => /* @__PURE__ */ jsx17(
|
|
2617
|
+
Pressable5,
|
|
2618
|
+
{
|
|
2619
|
+
onPress: () => setBrush({ ...brush, color }),
|
|
2620
|
+
style: {
|
|
2621
|
+
width: 26,
|
|
2622
|
+
height: 26,
|
|
2623
|
+
borderRadius: 13,
|
|
2624
|
+
backgroundColor: color,
|
|
2625
|
+
borderWidth: brush.color === color ? 3 : 1,
|
|
2626
|
+
borderColor: brush.color === color ? theme.colors.accent : theme.colors.track
|
|
2627
|
+
}
|
|
2628
|
+
},
|
|
2629
|
+
color
|
|
2630
|
+
))
|
|
2631
|
+
]
|
|
2632
|
+
}
|
|
2633
|
+
),
|
|
2634
|
+
/* @__PURE__ */ jsx17(
|
|
2635
|
+
ScrollView4,
|
|
2636
|
+
{
|
|
2637
|
+
horizontal: true,
|
|
2638
|
+
showsHorizontalScrollIndicator: false,
|
|
2639
|
+
contentContainerStyle: { gap: theme.spacing.sm, padding: theme.spacing.md },
|
|
2640
|
+
style: { flexGrow: 0 },
|
|
2641
|
+
children: BRUSHES.map((b) => /* @__PURE__ */ jsx17(
|
|
2642
|
+
Pressable5,
|
|
2643
|
+
{
|
|
2644
|
+
onPress: () => setBrush({ ...brush, brush: b.id }),
|
|
2645
|
+
style: {
|
|
2646
|
+
paddingHorizontal: theme.spacing.md,
|
|
2647
|
+
paddingVertical: theme.spacing.sm,
|
|
2648
|
+
borderRadius: theme.radii.md,
|
|
2649
|
+
backgroundColor: brush.brush === b.id ? theme.colors.surface : "transparent"
|
|
2650
|
+
},
|
|
2651
|
+
children: /* @__PURE__ */ jsx17(
|
|
2652
|
+
Text8,
|
|
2653
|
+
{
|
|
2654
|
+
style: {
|
|
2655
|
+
color: brush.brush === b.id ? theme.colors.accent : theme.colors.text,
|
|
2656
|
+
fontSize: theme.font.sizeSm,
|
|
2657
|
+
fontWeight: theme.font.weight
|
|
2658
|
+
},
|
|
2659
|
+
children: b.label
|
|
2660
|
+
}
|
|
2661
|
+
)
|
|
2662
|
+
},
|
|
2663
|
+
b.id
|
|
2664
|
+
))
|
|
2665
|
+
}
|
|
2666
|
+
)
|
|
2667
|
+
] });
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// src/panels/FilterStrip.tsx
|
|
2671
|
+
import { baseImageLayer, useEditor as useEditor12, useLuts } from "@pictor-sdk/core";
|
|
2672
|
+
import { useRenderer as useRenderer3 } from "@pictor-sdk/skia";
|
|
2673
|
+
import { Canvas, Image as SkiaImage } from "@shopify/react-native-skia";
|
|
2674
|
+
import { useEffect as useEffect5, useState as useState7 } from "react";
|
|
2675
|
+
import { Pressable as Pressable6, ScrollView as ScrollView5, Text as Text9, View as View10 } from "react-native";
|
|
2676
|
+
import { jsx as jsx18, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2677
|
+
var THUMB = 84;
|
|
2678
|
+
function useBaseImageLayer() {
|
|
2679
|
+
const { document } = useEditor12();
|
|
2680
|
+
return baseImageLayer(document);
|
|
2681
|
+
}
|
|
2682
|
+
function Thumb({ lut, label, active }) {
|
|
2683
|
+
const { document } = useEditor12();
|
|
2684
|
+
const renderer = useRenderer3();
|
|
2685
|
+
const theme = useTheme();
|
|
2686
|
+
const [image, setImage] = useState7(null);
|
|
2687
|
+
const lutId = lut?.id;
|
|
2688
|
+
const lutIntensity = lut?.intensity;
|
|
2689
|
+
useEffect5(() => {
|
|
2690
|
+
let alive = true;
|
|
2691
|
+
const requestedLut = lutId === void 0 ? void 0 : { id: lutId, intensity: lutIntensity ?? 1 };
|
|
2692
|
+
renderer.renderThumbnailImage(document, requestedLut, THUMB).then((img) => alive && setImage(img)).catch(() => {
|
|
2693
|
+
});
|
|
2694
|
+
return () => {
|
|
2695
|
+
alive = false;
|
|
2696
|
+
};
|
|
2697
|
+
}, [renderer, document, lutId, lutIntensity]);
|
|
2698
|
+
return /* @__PURE__ */ jsxs10(View10, { style: { alignItems: "center", gap: 4 }, children: [
|
|
2699
|
+
/* @__PURE__ */ jsx18(
|
|
2700
|
+
Text9,
|
|
2701
|
+
{
|
|
2702
|
+
style: {
|
|
2703
|
+
color: active ? theme.colors.accent : theme.colors.textMuted,
|
|
2704
|
+
fontSize: theme.font.sizeSm
|
|
2705
|
+
},
|
|
2706
|
+
children: label
|
|
2707
|
+
}
|
|
2708
|
+
),
|
|
2709
|
+
/* @__PURE__ */ jsx18(
|
|
2710
|
+
View10,
|
|
2711
|
+
{
|
|
2712
|
+
style: {
|
|
2713
|
+
width: THUMB,
|
|
2714
|
+
height: THUMB,
|
|
2715
|
+
borderRadius: theme.radii.sm,
|
|
2716
|
+
overflow: "hidden",
|
|
2717
|
+
borderWidth: active ? 2 : 0,
|
|
2718
|
+
borderColor: theme.colors.accent,
|
|
2719
|
+
backgroundColor: theme.colors.surface
|
|
2720
|
+
},
|
|
2721
|
+
children: image ? /* @__PURE__ */ jsx18(Canvas, { style: { width: THUMB, height: THUMB }, children: /* @__PURE__ */ jsx18(SkiaImage, { image, x: 0, y: 0, width: THUMB, height: THUMB, fit: "cover" }) }) : null
|
|
2722
|
+
}
|
|
2723
|
+
)
|
|
2724
|
+
] });
|
|
2725
|
+
}
|
|
2726
|
+
function FilterStrip() {
|
|
2727
|
+
const { dispatch } = useEditor12();
|
|
2728
|
+
const luts = useLuts();
|
|
2729
|
+
const theme = useTheme();
|
|
2730
|
+
const layer = useTargetImageLayer();
|
|
2731
|
+
const [intensityFor, setIntensityFor] = useState7(null);
|
|
2732
|
+
if (!layer) return null;
|
|
2733
|
+
const color = layer.data.color;
|
|
2734
|
+
const activeLut = color.lut;
|
|
2735
|
+
const applyLut = (lutId) => {
|
|
2736
|
+
if (lutId === null) {
|
|
2737
|
+
dispatch({ type: "layer/patchData", id: layer.id, patch: { color: { lut: void 0 } } });
|
|
2738
|
+
setIntensityFor(null);
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
if (activeLut?.id === lutId) {
|
|
2742
|
+
setIntensityFor((cur) => cur === lutId ? null : lutId);
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
dispatch({
|
|
2746
|
+
type: "layer/patchData",
|
|
2747
|
+
id: layer.id,
|
|
2748
|
+
patch: { color: { lut: { id: lutId, intensity: 1 } } }
|
|
2749
|
+
});
|
|
2750
|
+
};
|
|
2751
|
+
return /* @__PURE__ */ jsxs10(View10, { style: { backgroundColor: theme.colors.background }, children: [
|
|
2752
|
+
intensityFor && activeLut?.id === intensityFor ? /* @__PURE__ */ jsx18(View10, { style: { paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ jsx18(
|
|
2753
|
+
Slider,
|
|
2754
|
+
{
|
|
2755
|
+
value: activeLut.intensity,
|
|
2756
|
+
min: 0,
|
|
2757
|
+
max: 1,
|
|
2758
|
+
centered: false,
|
|
2759
|
+
valueLabel: `${Math.round(activeLut.intensity * 100)}`,
|
|
2760
|
+
onStart: () => dispatch({ type: "history/beginBatch", label: "intensity" }),
|
|
2761
|
+
onChange: (intensity) => dispatch({
|
|
2762
|
+
type: "layer/patchData",
|
|
2763
|
+
id: layer.id,
|
|
2764
|
+
patch: { color: { lut: { id: intensityFor, intensity } } }
|
|
2765
|
+
}),
|
|
2766
|
+
onEnd: () => dispatch({ type: "history/endBatch" })
|
|
2767
|
+
}
|
|
2768
|
+
) }) : null,
|
|
2769
|
+
/* @__PURE__ */ jsxs10(
|
|
2770
|
+
ScrollView5,
|
|
2771
|
+
{
|
|
2772
|
+
horizontal: true,
|
|
2773
|
+
showsHorizontalScrollIndicator: false,
|
|
2774
|
+
contentContainerStyle: { gap: theme.spacing.sm, padding: theme.spacing.md },
|
|
2775
|
+
style: { flexGrow: 0 },
|
|
2776
|
+
children: [
|
|
2777
|
+
/* @__PURE__ */ jsx18(Pressable6, { onPress: () => applyLut(null), children: /* @__PURE__ */ jsx18(Thumb, { lut: void 0, label: "Original", active: !activeLut }) }),
|
|
2778
|
+
luts.map((lut) => /* @__PURE__ */ jsx18(Pressable6, { onPress: () => applyLut(lut.id), children: /* @__PURE__ */ jsx18(
|
|
2779
|
+
Thumb,
|
|
2780
|
+
{
|
|
2781
|
+
lut: { id: lut.id, intensity: 1 },
|
|
2782
|
+
label: lut.label,
|
|
2783
|
+
active: activeLut?.id === lut.id
|
|
2784
|
+
}
|
|
2785
|
+
) }, lut.id))
|
|
2786
|
+
]
|
|
2787
|
+
}
|
|
2788
|
+
)
|
|
2789
|
+
] });
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// src/panels/LayoutPanel.tsx
|
|
2793
|
+
import { useEditor as useEditor13 } from "@pictor-sdk/core";
|
|
2794
|
+
import { Pressable as Pressable7, ScrollView as ScrollView6, Text as Text10 } from "react-native";
|
|
2795
|
+
|
|
2796
|
+
// src/panels/layoutMath.ts
|
|
2797
|
+
function layoutCells(preset, _doc) {
|
|
2798
|
+
switch (preset) {
|
|
2799
|
+
case "1":
|
|
2800
|
+
return [{ x: 0.5, y: 0.5, scale: 1 }];
|
|
2801
|
+
case "2v":
|
|
2802
|
+
return [
|
|
2803
|
+
{ x: 0.5, y: 0.25, scale: 1 },
|
|
2804
|
+
{ x: 0.5, y: 0.75, scale: 1 }
|
|
2805
|
+
];
|
|
2806
|
+
case "2h":
|
|
2807
|
+
return [
|
|
2808
|
+
{ x: 0.25, y: 0.5, scale: 0.5 },
|
|
2809
|
+
{ x: 0.75, y: 0.5, scale: 0.5 }
|
|
2810
|
+
];
|
|
2811
|
+
case "3v":
|
|
2812
|
+
return [
|
|
2813
|
+
{ x: 0.5, y: 1 / 6, scale: 1 },
|
|
2814
|
+
{ x: 0.5, y: 0.5, scale: 1 },
|
|
2815
|
+
{ x: 0.5, y: 5 / 6, scale: 1 }
|
|
2816
|
+
];
|
|
2817
|
+
case "4grid":
|
|
2818
|
+
return [
|
|
2819
|
+
{ x: 0.25, y: 0.25, scale: 0.5 },
|
|
2820
|
+
{ x: 0.75, y: 0.25, scale: 0.5 },
|
|
2821
|
+
{ x: 0.25, y: 0.75, scale: 0.5 },
|
|
2822
|
+
{ x: 0.75, y: 0.75, scale: 0.5 }
|
|
2823
|
+
];
|
|
2824
|
+
default: {
|
|
2825
|
+
const _exhaustive = preset;
|
|
2826
|
+
void _exhaustive;
|
|
2827
|
+
return [{ x: 0.5, y: 0.5, scale: 1 }];
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
function cellToTransformPatch(cell) {
|
|
2832
|
+
return { x: cell.x, y: cell.y, scale: cell.scale, rotation: 0 };
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// src/panels/LayoutPanel.tsx
|
|
2836
|
+
import { jsx as jsx19 } from "react/jsx-runtime";
|
|
2837
|
+
var PRESETS = [
|
|
2838
|
+
{ id: "1", label: "Single" },
|
|
2839
|
+
{ id: "2v", label: "2 rows" },
|
|
2840
|
+
{ id: "2h", label: "2 cols" },
|
|
2841
|
+
{ id: "3v", label: "3 rows" },
|
|
2842
|
+
{ id: "4grid", label: "Grid" }
|
|
2843
|
+
];
|
|
2844
|
+
function LayoutPanel() {
|
|
2845
|
+
const { document, dispatch } = useEditor13();
|
|
2846
|
+
const theme = useTheme();
|
|
2847
|
+
const apply = (preset) => {
|
|
2848
|
+
const cells = layoutCells(preset, document.size);
|
|
2849
|
+
const images = document.layers.filter((l) => l.type === "image");
|
|
2850
|
+
dispatch({ type: "history/beginBatch", label: "layout" });
|
|
2851
|
+
images.slice(0, cells.length).forEach((layer, i) => {
|
|
2852
|
+
dispatch({ type: "layer/transform", id: layer.id, patch: cellToTransformPatch(cells[i]) });
|
|
2853
|
+
});
|
|
2854
|
+
dispatch({ type: "history/endBatch" });
|
|
2855
|
+
};
|
|
2856
|
+
return /* @__PURE__ */ jsx19(
|
|
2857
|
+
ScrollView6,
|
|
2858
|
+
{
|
|
2859
|
+
horizontal: true,
|
|
2860
|
+
showsHorizontalScrollIndicator: false,
|
|
2861
|
+
contentContainerStyle: { gap: theme.spacing.sm, padding: theme.spacing.md },
|
|
2862
|
+
style: { flexGrow: 0, backgroundColor: theme.colors.background },
|
|
2863
|
+
children: PRESETS.map((preset) => /* @__PURE__ */ jsx19(
|
|
2864
|
+
Pressable7,
|
|
2865
|
+
{
|
|
2866
|
+
onPress: () => apply(preset.id),
|
|
2867
|
+
style: {
|
|
2868
|
+
paddingHorizontal: theme.spacing.md,
|
|
2869
|
+
paddingVertical: theme.spacing.sm,
|
|
2870
|
+
borderRadius: theme.radii.md,
|
|
2871
|
+
backgroundColor: theme.colors.surface
|
|
2872
|
+
},
|
|
2873
|
+
children: /* @__PURE__ */ jsx19(Text10, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm, fontWeight: theme.font.weight }, children: preset.label })
|
|
2874
|
+
},
|
|
2875
|
+
preset.id
|
|
2876
|
+
))
|
|
2877
|
+
}
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// src/panels/LuxPanel.tsx
|
|
2882
|
+
import {
|
|
2883
|
+
luxControlState,
|
|
2884
|
+
luxIntensityPatch,
|
|
2885
|
+
useEditor as useEditor14
|
|
2886
|
+
} from "@pictor-sdk/core";
|
|
2887
|
+
import { useLuxAnalysis } from "@pictor-sdk/skia";
|
|
2888
|
+
import { Pressable as Pressable8, Text as Text11, View as View11 } from "react-native";
|
|
2889
|
+
import { jsx as jsx20, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2890
|
+
function LuxPanel() {
|
|
2891
|
+
const { dispatch } = useEditor14();
|
|
2892
|
+
const theme = useTheme();
|
|
2893
|
+
const layer = useTargetImageLayer();
|
|
2894
|
+
const data = layer?.data;
|
|
2895
|
+
const proposal = useLuxAnalysis(data?.source);
|
|
2896
|
+
if (!layer || !data) return null;
|
|
2897
|
+
const stored = data.color.lux;
|
|
2898
|
+
const intensity = stored?.intensity ?? 0;
|
|
2899
|
+
const state = luxControlState(stored, proposal);
|
|
2900
|
+
const setIntensity = (value) => {
|
|
2901
|
+
const lux = luxIntensityPatch(state, value);
|
|
2902
|
+
if (!lux) return;
|
|
2903
|
+
dispatch({ type: "layer/patchData", id: layer.id, patch: { color: { lux } } });
|
|
2904
|
+
};
|
|
2905
|
+
const reset = () => dispatch({
|
|
2906
|
+
type: "layer/patchData",
|
|
2907
|
+
id: layer.id,
|
|
2908
|
+
patch: { color: { lux: void 0 } }
|
|
2909
|
+
});
|
|
2910
|
+
return /* @__PURE__ */ jsx20(View11, { style: { backgroundColor: theme.colors.background, paddingBottom: theme.spacing.md }, children: /* @__PURE__ */ jsxs11(View11, { style: { paddingHorizontal: theme.spacing.lg }, children: [
|
|
2911
|
+
/* @__PURE__ */ jsxs11(
|
|
2912
|
+
View11,
|
|
2913
|
+
{
|
|
2914
|
+
style: {
|
|
2915
|
+
flexDirection: "row",
|
|
2916
|
+
alignItems: "center",
|
|
2917
|
+
justifyContent: "center",
|
|
2918
|
+
gap: theme.spacing.sm,
|
|
2919
|
+
minHeight: 28
|
|
2920
|
+
},
|
|
2921
|
+
children: [
|
|
2922
|
+
/* @__PURE__ */ jsx20(
|
|
2923
|
+
Text11,
|
|
2924
|
+
{
|
|
2925
|
+
style: {
|
|
2926
|
+
color: theme.colors.textMuted,
|
|
2927
|
+
textAlign: "center",
|
|
2928
|
+
fontSize: theme.font.sizeSm
|
|
2929
|
+
},
|
|
2930
|
+
children: state.pending ? "Analyzing\u2026" : `Lux ${intensity === 0 ? "" : Math.round(intensity * 100)}`
|
|
2931
|
+
}
|
|
2932
|
+
),
|
|
2933
|
+
intensity > 0 ? /* @__PURE__ */ jsx20(
|
|
2934
|
+
Pressable8,
|
|
2935
|
+
{
|
|
2936
|
+
accessibilityRole: "button",
|
|
2937
|
+
accessibilityLabel: "Reset Lux",
|
|
2938
|
+
onPress: reset,
|
|
2939
|
+
hitSlop: 8,
|
|
2940
|
+
style: {
|
|
2941
|
+
paddingHorizontal: theme.spacing.sm,
|
|
2942
|
+
paddingVertical: 2,
|
|
2943
|
+
borderRadius: theme.radii.sm,
|
|
2944
|
+
borderWidth: 1,
|
|
2945
|
+
borderColor: theme.colors.track
|
|
2946
|
+
},
|
|
2947
|
+
children: /* @__PURE__ */ jsx20(Text11, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: "Reset" })
|
|
2948
|
+
}
|
|
2949
|
+
) : null
|
|
2950
|
+
]
|
|
2951
|
+
}
|
|
2952
|
+
),
|
|
2953
|
+
/* @__PURE__ */ jsx20(
|
|
2954
|
+
Slider,
|
|
2955
|
+
{
|
|
2956
|
+
value: intensity,
|
|
2957
|
+
min: 0,
|
|
2958
|
+
max: 1,
|
|
2959
|
+
centered: false,
|
|
2960
|
+
disabled: !state.interactive,
|
|
2961
|
+
testID: "lux-slider",
|
|
2962
|
+
valueLabel: `${Math.round(intensity * 100)}`,
|
|
2963
|
+
onStart: () => dispatch({ type: "history/beginBatch", label: "lux" }),
|
|
2964
|
+
onChange: setIntensity,
|
|
2965
|
+
onEnd: () => dispatch({ type: "history/endBatch" })
|
|
2966
|
+
}
|
|
2967
|
+
)
|
|
2968
|
+
] }) });
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// src/panels/MediaPanel.tsx
|
|
2972
|
+
import {
|
|
2973
|
+
initialMediaTransform,
|
|
2974
|
+
mediaConfigFromProfile as mediaConfigFromProfile2,
|
|
2975
|
+
useEditor as useEditor15
|
|
2976
|
+
} from "@pictor-sdk/core";
|
|
2977
|
+
import { preferResolvedSize as preferResolvedSize2, useResolvedImageSize as useResolvedImageSize2 } from "@pictor-sdk/skia";
|
|
2978
|
+
import { Pressable as Pressable9, Text as Text12, View as View12 } from "react-native";
|
|
2979
|
+
import { jsx as jsx21, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2980
|
+
function MediaPanel() {
|
|
2981
|
+
const { document, dispatch, profile } = useEditor15();
|
|
2982
|
+
const layer = useBaseMediaLayer();
|
|
2983
|
+
const theme = useTheme();
|
|
2984
|
+
const data = layer?.data;
|
|
2985
|
+
const resolved = useResolvedImageSize2(data?.source);
|
|
2986
|
+
if (!layer) {
|
|
2987
|
+
return /* @__PURE__ */ jsx21(View12, { style: { padding: theme.spacing.lg }, children: /* @__PURE__ */ jsx21(Text12, { style: { color: theme.colors.textMuted, fontSize: theme.font.sizeSm }, children: "This document has no base photo." }) });
|
|
2988
|
+
}
|
|
2989
|
+
const reset = () => {
|
|
2990
|
+
const config = mediaConfigFromProfile2(profile.interaction?.media);
|
|
2991
|
+
dispatch({
|
|
2992
|
+
type: "layer/transform",
|
|
2993
|
+
id: layer.id,
|
|
2994
|
+
patch: initialMediaTransform(
|
|
2995
|
+
preferResolvedSize2(resolved, data?.intrinsic),
|
|
2996
|
+
document.size,
|
|
2997
|
+
config
|
|
2998
|
+
)
|
|
2999
|
+
});
|
|
3000
|
+
};
|
|
3001
|
+
return /* @__PURE__ */ jsxs12(View12, { style: { padding: theme.spacing.lg, gap: theme.spacing.md }, children: [
|
|
3002
|
+
/* @__PURE__ */ jsx21(Text12, { style: { color: theme.colors.textMuted, fontSize: theme.font.sizeSm }, children: "Drag to reposition \xB7 pinch to zoom \xB7 twist to rotate \xB7 double-tap to zoom" }),
|
|
3003
|
+
/* @__PURE__ */ jsx21(
|
|
3004
|
+
Pressable9,
|
|
3005
|
+
{
|
|
3006
|
+
accessibilityRole: "button",
|
|
3007
|
+
accessibilityLabel: "Reset photo placement",
|
|
3008
|
+
onPress: reset,
|
|
3009
|
+
style: {
|
|
3010
|
+
alignSelf: "flex-start",
|
|
3011
|
+
paddingHorizontal: theme.spacing.md,
|
|
3012
|
+
paddingVertical: theme.spacing.sm,
|
|
3013
|
+
borderRadius: theme.radii.md,
|
|
3014
|
+
backgroundColor: theme.colors.surface
|
|
3015
|
+
},
|
|
3016
|
+
children: /* @__PURE__ */ jsx21(Text12, { style: { color: theme.colors.text, fontSize: theme.font.size }, children: "Reset" })
|
|
3017
|
+
}
|
|
3018
|
+
)
|
|
3019
|
+
] });
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
// src/panels/LayersPanel.tsx
|
|
3023
|
+
import {
|
|
3024
|
+
isDirectManipulationLayer as isDirectManipulationLayer2,
|
|
3025
|
+
useEditor as useEditor16
|
|
3026
|
+
} from "@pictor-sdk/core";
|
|
3027
|
+
import { Pressable as Pressable10, ScrollView as ScrollView7, Text as Text13, View as View13 } from "react-native";
|
|
3028
|
+
import { jsx as jsx22, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3029
|
+
function LayersPanel() {
|
|
3030
|
+
const { document, selection, select, dispatch, profile } = useEditor16();
|
|
3031
|
+
const theme = useTheme();
|
|
3032
|
+
const layers = layersForPanel(document, profile.interaction?.layers);
|
|
3033
|
+
const minimumIndex = minimumLayerIndexForPanel(document, profile.interaction?.layers);
|
|
3034
|
+
return /* @__PURE__ */ jsx22(ScrollView7, { style: { maxHeight: 220, backgroundColor: theme.colors.background }, children: layers.map(({ layer, index }) => {
|
|
3035
|
+
const selected = selection.includes(layer.id);
|
|
3036
|
+
return /* @__PURE__ */ jsxs13(
|
|
3037
|
+
View13,
|
|
3038
|
+
{
|
|
3039
|
+
style: {
|
|
3040
|
+
flexDirection: "row",
|
|
3041
|
+
alignItems: "center",
|
|
3042
|
+
paddingHorizontal: theme.spacing.md,
|
|
3043
|
+
paddingVertical: theme.spacing.xs,
|
|
3044
|
+
backgroundColor: selected ? theme.colors.surface : "transparent"
|
|
3045
|
+
},
|
|
3046
|
+
children: [
|
|
3047
|
+
/* @__PURE__ */ jsx22(Pressable10, { style: { flex: 1 }, onPress: () => select([layer.id]), children: /* @__PURE__ */ jsx22(Text13, { style: { color: selected ? theme.colors.accent : theme.colors.text, fontSize: theme.font.sizeSm }, children: layer.name ?? `${layer.type} \xB7 ${layer.id.slice(0, 6)}` }) }),
|
|
3048
|
+
/* @__PURE__ */ jsx22(
|
|
3049
|
+
Pressable10,
|
|
3050
|
+
{
|
|
3051
|
+
disabled: index === document.layers.length - 1,
|
|
3052
|
+
onPress: () => dispatch({ type: "layer/reorder", id: layer.id, to: index + 1 }),
|
|
3053
|
+
style: { padding: theme.spacing.xs },
|
|
3054
|
+
children: /* @__PURE__ */ jsx22(Text13, { style: { color: theme.colors.textMuted }, children: "\u25B2" })
|
|
3055
|
+
}
|
|
3056
|
+
),
|
|
3057
|
+
/* @__PURE__ */ jsx22(
|
|
3058
|
+
Pressable10,
|
|
3059
|
+
{
|
|
3060
|
+
disabled: index === minimumIndex,
|
|
3061
|
+
onPress: () => dispatch({ type: "layer/reorder", id: layer.id, to: index - 1 }),
|
|
3062
|
+
style: { padding: theme.spacing.xs },
|
|
3063
|
+
children: /* @__PURE__ */ jsx22(Text13, { style: { color: theme.colors.textMuted }, children: "\u25BC" })
|
|
3064
|
+
}
|
|
3065
|
+
),
|
|
3066
|
+
/* @__PURE__ */ jsx22(
|
|
3067
|
+
Pressable10,
|
|
3068
|
+
{
|
|
3069
|
+
onPress: () => dispatch({ type: "layer/remove", id: layer.id }),
|
|
3070
|
+
style: { padding: theme.spacing.xs },
|
|
3071
|
+
children: /* @__PURE__ */ jsx22(Text13, { style: { color: theme.colors.danger }, children: "\u2715" })
|
|
3072
|
+
}
|
|
3073
|
+
)
|
|
3074
|
+
]
|
|
3075
|
+
},
|
|
3076
|
+
layer.id
|
|
3077
|
+
);
|
|
3078
|
+
}) });
|
|
3079
|
+
}
|
|
3080
|
+
function minimumLayerIndexForPanel(document, interaction) {
|
|
3081
|
+
return interaction?.excludeBaseImage && document.layers[0]?.type === "image" ? 1 : 0;
|
|
3082
|
+
}
|
|
3083
|
+
function layersForPanel(document, interaction) {
|
|
3084
|
+
return document.layers.map((layer, index) => ({ layer, index })).filter(({ layer, index }) => isDirectManipulationLayer2(layer, index, interaction)).reverse();
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
// src/panels/PerspectivePanel.tsx
|
|
3088
|
+
import {
|
|
3089
|
+
needsPerspective,
|
|
3090
|
+
perspectivePatch,
|
|
3091
|
+
useEditor as useEditor17,
|
|
3092
|
+
MAX_PERSPECTIVE_DEGREES
|
|
3093
|
+
} from "@pictor-sdk/core";
|
|
3094
|
+
import { useState as useState8 } from "react";
|
|
3095
|
+
import { Pressable as Pressable11, Text as Text14, View as View14 } from "react-native";
|
|
3096
|
+
import { jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3097
|
+
var MAX_STRAIGHTEN_DEGREES = MAX_PERSPECTIVE_DEGREES;
|
|
3098
|
+
var MODES = [
|
|
3099
|
+
{ key: "vertical", label: "Vertical" },
|
|
3100
|
+
{ key: "straighten", label: "Straighten" },
|
|
3101
|
+
{ key: "horizontal", label: "Horizontal" }
|
|
3102
|
+
];
|
|
3103
|
+
var toRadians = (degrees) => degrees * Math.PI / 180;
|
|
3104
|
+
var toDegrees = (radians) => radians * 180 / Math.PI;
|
|
3105
|
+
function PerspectivePanel() {
|
|
3106
|
+
const { dispatch } = useEditor17();
|
|
3107
|
+
const theme = useTheme();
|
|
3108
|
+
const layer = useTargetImageLayer();
|
|
3109
|
+
const [mode, setMode] = useState8("vertical");
|
|
3110
|
+
if (!layer) return null;
|
|
3111
|
+
const data = layer.data;
|
|
3112
|
+
const perspective = data.perspective ?? { vertical: 0, horizontal: 0 };
|
|
3113
|
+
const straightenDegrees = toDegrees(data.straighten ?? 0);
|
|
3114
|
+
const degreesOf = (key) => key === "straighten" ? straightenDegrees : perspective[key] * MAX_PERSPECTIVE_DEGREES;
|
|
3115
|
+
const setDegrees = (key, degrees) => {
|
|
3116
|
+
if (key === "straighten") {
|
|
3117
|
+
return dispatch({
|
|
3118
|
+
type: "layer/patchData",
|
|
3119
|
+
id: layer.id,
|
|
3120
|
+
patch: { straighten: degrees === 0 ? void 0 : toRadians(degrees) }
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
const next = { ...perspective, [key]: degrees / MAX_PERSPECTIVE_DEGREES };
|
|
3124
|
+
dispatch({
|
|
3125
|
+
type: "layer/patchData",
|
|
3126
|
+
id: layer.id,
|
|
3127
|
+
// `perspectivePatch` deletes the key when both axes reach zero, so
|
|
3128
|
+
// straightening a correction back out leaves nothing serialized.
|
|
3129
|
+
patch: perspectivePatch(needsPerspective(next) ? next : void 0)
|
|
3130
|
+
});
|
|
3131
|
+
};
|
|
3132
|
+
const value = degreesOf(mode);
|
|
3133
|
+
const max = mode === "straighten" ? MAX_STRAIGHTEN_DEGREES : MAX_PERSPECTIVE_DEGREES;
|
|
3134
|
+
return /* @__PURE__ */ jsxs14(View14, { style: { backgroundColor: theme.colors.background, paddingBottom: theme.spacing.md }, children: [
|
|
3135
|
+
/* @__PURE__ */ jsx23(
|
|
3136
|
+
View14,
|
|
3137
|
+
{
|
|
3138
|
+
style: {
|
|
3139
|
+
flexDirection: "row",
|
|
3140
|
+
justifyContent: "center",
|
|
3141
|
+
gap: theme.spacing.sm,
|
|
3142
|
+
paddingVertical: theme.spacing.sm
|
|
3143
|
+
},
|
|
3144
|
+
children: MODES.map(({ key, label }) => {
|
|
3145
|
+
const selected = key === mode;
|
|
3146
|
+
const degrees = degreesOf(key);
|
|
3147
|
+
const set = Math.abs(degrees) > 0.05;
|
|
3148
|
+
return /* @__PURE__ */ jsxs14(
|
|
3149
|
+
Pressable11,
|
|
3150
|
+
{
|
|
3151
|
+
accessibilityRole: "button",
|
|
3152
|
+
accessibilityLabel: `${label} correction`,
|
|
3153
|
+
onPress: () => setMode(key),
|
|
3154
|
+
style: {
|
|
3155
|
+
flexDirection: "row",
|
|
3156
|
+
alignItems: "center",
|
|
3157
|
+
gap: 6,
|
|
3158
|
+
paddingHorizontal: theme.spacing.sm,
|
|
3159
|
+
paddingVertical: 6,
|
|
3160
|
+
borderRadius: theme.radii.sm,
|
|
3161
|
+
borderWidth: 1,
|
|
3162
|
+
borderColor: selected ? theme.colors.accent : theme.colors.track
|
|
3163
|
+
},
|
|
3164
|
+
children: [
|
|
3165
|
+
/* @__PURE__ */ jsx23(
|
|
3166
|
+
Text14,
|
|
3167
|
+
{
|
|
3168
|
+
style: {
|
|
3169
|
+
color: selected ? theme.colors.accent : theme.colors.textMuted,
|
|
3170
|
+
fontSize: theme.font.sizeSm,
|
|
3171
|
+
fontWeight: theme.font.weight
|
|
3172
|
+
},
|
|
3173
|
+
children: set ? `${label} ${degrees.toFixed(1)}\xB0` : label
|
|
3174
|
+
}
|
|
3175
|
+
),
|
|
3176
|
+
set ? /* @__PURE__ */ jsx23(
|
|
3177
|
+
Pressable11,
|
|
3178
|
+
{
|
|
3179
|
+
accessibilityRole: "button",
|
|
3180
|
+
accessibilityLabel: `Reset ${label.toLowerCase()}`,
|
|
3181
|
+
onPress: () => setDegrees(key, 0),
|
|
3182
|
+
hitSlop: 8,
|
|
3183
|
+
children: /* @__PURE__ */ jsx23(Text14, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: "\u2715" })
|
|
3184
|
+
}
|
|
3185
|
+
) : null
|
|
3186
|
+
]
|
|
3187
|
+
},
|
|
3188
|
+
key
|
|
3189
|
+
);
|
|
3190
|
+
})
|
|
3191
|
+
}
|
|
3192
|
+
),
|
|
3193
|
+
/* @__PURE__ */ jsx23(View14, { style: { paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ jsx23(
|
|
3194
|
+
Slider,
|
|
3195
|
+
{
|
|
3196
|
+
value,
|
|
3197
|
+
min: -max,
|
|
3198
|
+
max,
|
|
3199
|
+
centered: true,
|
|
3200
|
+
testID: `perspective-${mode}`,
|
|
3201
|
+
valueLabel: `${value > 0 ? "+" : ""}${value.toFixed(1)}\xB0`,
|
|
3202
|
+
onStart: () => dispatch({ type: "history/beginBatch", label: `perspective-${mode}` }),
|
|
3203
|
+
onChange: (v) => setDegrees(mode, v),
|
|
3204
|
+
onEnd: () => dispatch({ type: "history/endBatch" })
|
|
3205
|
+
}
|
|
3206
|
+
) })
|
|
3207
|
+
] });
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
// src/panels/StickerPanel.tsx
|
|
3211
|
+
import { createId as createId2, useEditor as useEditor18 } from "@pictor-sdk/core";
|
|
3212
|
+
import { Image, Pressable as Pressable12, ScrollView as ScrollView8, Text as Text15, View as View15 } from "react-native";
|
|
3213
|
+
import { jsx as jsx24, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3214
|
+
function StickerPanel() {
|
|
3215
|
+
const { dispatch, select } = useEditor18();
|
|
3216
|
+
const theme = useTheme();
|
|
3217
|
+
const [stickers] = useUiState("stickers", []);
|
|
3218
|
+
const add = (sticker) => {
|
|
3219
|
+
const id = createId2("sticker");
|
|
3220
|
+
dispatch({
|
|
3221
|
+
type: "layer/add",
|
|
3222
|
+
layer: {
|
|
3223
|
+
id,
|
|
3224
|
+
type: "sticker",
|
|
3225
|
+
transform: { x: 0.5, y: 0.5, scale: 1, rotation: 0 },
|
|
3226
|
+
opacity: 1,
|
|
3227
|
+
blendMode: "normal",
|
|
3228
|
+
data: {
|
|
3229
|
+
source: sticker.source,
|
|
3230
|
+
intrinsic: sticker.intrinsic ?? { width: 0, height: 0 }
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
});
|
|
3234
|
+
select([id]);
|
|
3235
|
+
};
|
|
3236
|
+
if (stickers.length === 0) {
|
|
3237
|
+
return /* @__PURE__ */ jsx24(View15, { style: { backgroundColor: theme.colors.background, padding: theme.spacing.md }, children: /* @__PURE__ */ jsxs15(Text15, { style: { color: theme.colors.textMuted, textAlign: "center", fontSize: theme.font.sizeSm }, children: [
|
|
3238
|
+
"No stickers registered \u2014 pass `stickers` to ",
|
|
3239
|
+
"<Editor>",
|
|
3240
|
+
"."
|
|
3241
|
+
] }) });
|
|
3242
|
+
}
|
|
3243
|
+
return /* @__PURE__ */ jsx24(
|
|
3244
|
+
ScrollView8,
|
|
3245
|
+
{
|
|
3246
|
+
horizontal: true,
|
|
3247
|
+
showsHorizontalScrollIndicator: false,
|
|
3248
|
+
contentContainerStyle: { gap: theme.spacing.sm, padding: theme.spacing.md },
|
|
3249
|
+
style: { flexGrow: 0, backgroundColor: theme.colors.background },
|
|
3250
|
+
children: stickers.map((sticker) => /* @__PURE__ */ jsx24(Pressable12, { onPress: () => add(sticker), children: /* @__PURE__ */ jsx24(
|
|
3251
|
+
Image,
|
|
3252
|
+
{
|
|
3253
|
+
source: sticker.source,
|
|
3254
|
+
style: { width: 64, height: 64, borderRadius: theme.radii.sm, backgroundColor: theme.colors.surface },
|
|
3255
|
+
resizeMode: "contain"
|
|
3256
|
+
}
|
|
3257
|
+
) }, sticker.id))
|
|
3258
|
+
}
|
|
3259
|
+
);
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
// src/panels/TiltShiftPanel.tsx
|
|
3263
|
+
import {
|
|
3264
|
+
createTiltShift,
|
|
3265
|
+
needsTiltShift as needsTiltShift2,
|
|
3266
|
+
tiltShiftPatch as tiltShiftPatch2,
|
|
3267
|
+
useEditor as useEditor19,
|
|
3268
|
+
MAX_TILT_BLUR,
|
|
3269
|
+
MAX_TILT_FEATHER,
|
|
3270
|
+
MAX_TILT_RADIUS
|
|
3271
|
+
} from "@pictor-sdk/core";
|
|
3272
|
+
import { Pressable as Pressable13, Text as Text16, View as View16 } from "react-native";
|
|
3273
|
+
import { jsx as jsx25, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
3274
|
+
var MODES2 = [
|
|
3275
|
+
{ key: "off", label: "Off" },
|
|
3276
|
+
{ key: "radial", label: "Radial" },
|
|
3277
|
+
{ key: "linear", label: "Linear" }
|
|
3278
|
+
];
|
|
3279
|
+
function TiltShiftPanel() {
|
|
3280
|
+
const { dispatch } = useEditor19();
|
|
3281
|
+
const theme = useTheme();
|
|
3282
|
+
const layer = useTargetImageLayer();
|
|
3283
|
+
if (!layer) return null;
|
|
3284
|
+
const data = layer.data;
|
|
3285
|
+
const tilt = needsTiltShift2(data.tiltShift) ? data.tiltShift : void 0;
|
|
3286
|
+
const mode = tilt?.mode ?? "off";
|
|
3287
|
+
const patch = (next) => dispatch({
|
|
3288
|
+
type: "layer/patchData",
|
|
3289
|
+
id: layer.id,
|
|
3290
|
+
patch: tiltShiftPatch2(next)
|
|
3291
|
+
});
|
|
3292
|
+
const setMode = (key) => {
|
|
3293
|
+
if (key === "off") return patch(void 0);
|
|
3294
|
+
patch(tilt ? { ...tilt, mode: key } : createTiltShift(key));
|
|
3295
|
+
};
|
|
3296
|
+
const set = (over) => tilt && patch({ ...tilt, ...over });
|
|
3297
|
+
const sliders = [
|
|
3298
|
+
{ key: "blur", label: "Blur", max: MAX_TILT_BLUR },
|
|
3299
|
+
{ key: "radius", label: "Size", max: MAX_TILT_RADIUS },
|
|
3300
|
+
{ key: "feather", label: "Feather", max: MAX_TILT_FEATHER }
|
|
3301
|
+
];
|
|
3302
|
+
return /* @__PURE__ */ jsxs16(View16, { style: { backgroundColor: theme.colors.background, paddingBottom: theme.spacing.md }, children: [
|
|
3303
|
+
/* @__PURE__ */ jsx25(
|
|
3304
|
+
View16,
|
|
3305
|
+
{
|
|
3306
|
+
style: {
|
|
3307
|
+
flexDirection: "row",
|
|
3308
|
+
justifyContent: "center",
|
|
3309
|
+
gap: theme.spacing.md,
|
|
3310
|
+
paddingVertical: theme.spacing.sm
|
|
3311
|
+
},
|
|
3312
|
+
children: MODES2.map(({ key, label }) => {
|
|
3313
|
+
const selected = key === mode;
|
|
3314
|
+
return /* @__PURE__ */ jsx25(
|
|
3315
|
+
Pressable13,
|
|
3316
|
+
{
|
|
3317
|
+
accessibilityRole: "button",
|
|
3318
|
+
accessibilityLabel: `Tilt shift ${label.toLowerCase()}`,
|
|
3319
|
+
onPress: () => setMode(key),
|
|
3320
|
+
style: {
|
|
3321
|
+
paddingHorizontal: theme.spacing.md,
|
|
3322
|
+
paddingVertical: 6,
|
|
3323
|
+
borderRadius: theme.radii.sm,
|
|
3324
|
+
borderWidth: 1,
|
|
3325
|
+
borderColor: selected ? theme.colors.accent : theme.colors.track
|
|
3326
|
+
},
|
|
3327
|
+
children: /* @__PURE__ */ jsx25(
|
|
3328
|
+
Text16,
|
|
3329
|
+
{
|
|
3330
|
+
style: {
|
|
3331
|
+
color: selected ? theme.colors.accent : theme.colors.textMuted,
|
|
3332
|
+
fontSize: theme.font.sizeSm,
|
|
3333
|
+
fontWeight: theme.font.weight
|
|
3334
|
+
},
|
|
3335
|
+
children: label
|
|
3336
|
+
}
|
|
3337
|
+
)
|
|
3338
|
+
},
|
|
3339
|
+
key
|
|
3340
|
+
);
|
|
3341
|
+
})
|
|
3342
|
+
}
|
|
3343
|
+
),
|
|
3344
|
+
/* @__PURE__ */ jsxs16(View16, { style: { paddingHorizontal: theme.spacing.lg }, children: [
|
|
3345
|
+
/* @__PURE__ */ jsx25(
|
|
3346
|
+
Text16,
|
|
3347
|
+
{
|
|
3348
|
+
style: {
|
|
3349
|
+
color: theme.colors.textMuted,
|
|
3350
|
+
textAlign: "center",
|
|
3351
|
+
fontSize: theme.font.sizeSm,
|
|
3352
|
+
minHeight: 20
|
|
3353
|
+
},
|
|
3354
|
+
children: tilt ? "Drag on the photo to place the focus" : "Off"
|
|
3355
|
+
}
|
|
3356
|
+
),
|
|
3357
|
+
sliders.map(({ key, label, max }) => {
|
|
3358
|
+
const value = tilt?.[key] ?? 0;
|
|
3359
|
+
return /* @__PURE__ */ jsxs16(View16, { children: [
|
|
3360
|
+
/* @__PURE__ */ jsxs16(Text16, { style: { color: theme.colors.textMuted, fontSize: theme.font.sizeSm }, children: [
|
|
3361
|
+
label,
|
|
3362
|
+
" ",
|
|
3363
|
+
tilt ? Math.round(value / max * 100) : ""
|
|
3364
|
+
] }),
|
|
3365
|
+
/* @__PURE__ */ jsx25(
|
|
3366
|
+
Slider,
|
|
3367
|
+
{
|
|
3368
|
+
value,
|
|
3369
|
+
min: 0,
|
|
3370
|
+
max,
|
|
3371
|
+
centered: false,
|
|
3372
|
+
testID: `tilt-${key}`,
|
|
3373
|
+
disabled: !tilt,
|
|
3374
|
+
onStart: () => dispatch({ type: "history/beginBatch", label: `tilt-${key}` }),
|
|
3375
|
+
onChange: (v) => set({ [key]: v }),
|
|
3376
|
+
onEnd: () => dispatch({ type: "history/endBatch" })
|
|
3377
|
+
}
|
|
3378
|
+
)
|
|
3379
|
+
] }, key);
|
|
3380
|
+
})
|
|
3381
|
+
] })
|
|
3382
|
+
] });
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
// src/ToolPanelHost.tsx
|
|
3386
|
+
import { jsx as jsx26, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
3387
|
+
function resolvePanel(toolId, pluginPanel, builtins = BUILTIN_PANELS) {
|
|
3388
|
+
if (!toolId) return null;
|
|
3389
|
+
if (pluginPanel) return pluginPanel;
|
|
3390
|
+
return builtins[toolId] ?? null;
|
|
3391
|
+
}
|
|
3392
|
+
var BUILTIN_PANELS = {
|
|
3393
|
+
media: MediaPanel,
|
|
3394
|
+
addPhoto: AddPhotoPanel,
|
|
3395
|
+
filter: FilterStrip,
|
|
3396
|
+
adjust: AdjustPanel,
|
|
3397
|
+
lux: LuxPanel,
|
|
3398
|
+
color: ColorPanel,
|
|
3399
|
+
tilt: TiltShiftPanel,
|
|
3400
|
+
perspective: PerspectivePanel,
|
|
3401
|
+
crop: CropPanel,
|
|
3402
|
+
draw: DrawPanel,
|
|
3403
|
+
sticker: StickerPanel,
|
|
3404
|
+
layout: LayoutPanel,
|
|
3405
|
+
layers: LayersPanel
|
|
3406
|
+
};
|
|
3407
|
+
function ToolPanelHost() {
|
|
3408
|
+
const { activeTool, document, selection, dispatch, setActiveTool } = useEditor20();
|
|
3409
|
+
const tools = useTools();
|
|
3410
|
+
const tool = tools.find((t) => t.id === activeTool);
|
|
3411
|
+
const Panel = resolvePanel(activeTool, tool?.Panel);
|
|
3412
|
+
const theme = useTheme();
|
|
3413
|
+
const finalized = useRef11(false);
|
|
3414
|
+
useEffect6(() => {
|
|
3415
|
+
if (!activeTool || !Panel) return;
|
|
3416
|
+
finalized.current = false;
|
|
3417
|
+
dispatch({ type: "history/beginBatch", label: activeTool });
|
|
3418
|
+
return () => {
|
|
3419
|
+
if (!finalized.current) dispatch({ type: "history/cancelBatch" });
|
|
3420
|
+
};
|
|
3421
|
+
}, [activeTool, Panel, dispatch]);
|
|
3422
|
+
if (!Panel) return null;
|
|
3423
|
+
const done = () => {
|
|
3424
|
+
finalized.current = true;
|
|
3425
|
+
dispatch({ type: "history/endBatch" });
|
|
3426
|
+
setActiveTool(null);
|
|
3427
|
+
};
|
|
3428
|
+
const cancel = () => {
|
|
3429
|
+
finalized.current = true;
|
|
3430
|
+
dispatch({ type: "history/cancelBatch" });
|
|
3431
|
+
setActiveTool(null);
|
|
3432
|
+
};
|
|
3433
|
+
return /* @__PURE__ */ jsxs17(View17, { style: { backgroundColor: theme.colors.background, borderTopLeftRadius: theme.radii.lg, borderTopRightRadius: theme.radii.lg, overflow: "hidden" }, children: [
|
|
3434
|
+
/* @__PURE__ */ jsx26(Panel, { document, selection, dispatch, close: done }),
|
|
3435
|
+
/* @__PURE__ */ jsxs17(View17, { style: { height: 52, flexDirection: "row", alignItems: "center", justifyContent: "space-between", borderTopWidth: 1, borderTopColor: theme.colors.track, paddingHorizontal: theme.spacing.lg }, children: [
|
|
3436
|
+
/* @__PURE__ */ jsx26(Pressable14, { accessibilityRole: "button", accessibilityLabel: "Cancel edits", onPress: cancel, children: /* @__PURE__ */ jsx26(Text17, { style: { color: theme.colors.text, fontSize: theme.font.size }, children: "Cancel" }) }),
|
|
3437
|
+
/* @__PURE__ */ jsx26(Text17, { style: { color: theme.colors.text, fontSize: theme.font.size, fontWeight: theme.font.weight }, children: tool?.label ?? activeTool }),
|
|
3438
|
+
/* @__PURE__ */ jsx26(Pressable14, { accessibilityRole: "button", accessibilityLabel: "Apply edits", onPress: done, children: /* @__PURE__ */ jsx26(Text17, { style: { color: theme.colors.accent, fontSize: theme.font.size, fontWeight: theme.font.weight }, children: "Done" }) })
|
|
3439
|
+
] })
|
|
3440
|
+
] });
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
// src/composer/TextComposer.tsx
|
|
3444
|
+
import {
|
|
3445
|
+
MAX_TEXT_SIZE,
|
|
3446
|
+
MIN_TEXT_SIZE,
|
|
3447
|
+
fontChoices,
|
|
3448
|
+
useEditor as useEditor22,
|
|
3449
|
+
useEditorStore as useEditorStore5
|
|
3450
|
+
} from "@pictor-sdk/core";
|
|
3451
|
+
import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo11, useRef as useRef13, useState as useState10 } from "react";
|
|
3452
|
+
import {
|
|
3453
|
+
BackHandler,
|
|
3454
|
+
Keyboard,
|
|
3455
|
+
Platform,
|
|
3456
|
+
Pressable as Pressable15,
|
|
3457
|
+
ScrollView as ScrollView9,
|
|
3458
|
+
Text as Text18,
|
|
3459
|
+
TextInput,
|
|
3460
|
+
View as View18
|
|
3461
|
+
} from "react-native";
|
|
3462
|
+
|
|
3463
|
+
// src/composer/keyboardLayout.ts
|
|
3464
|
+
var HIDDEN_KEYBOARD = { height: 0 };
|
|
3465
|
+
var MAX_DURATION = 400;
|
|
3466
|
+
var FALLBACK_DURATION = 180;
|
|
3467
|
+
function composerInsets(frame, safeAreaBottom = 0) {
|
|
3468
|
+
const height = Number.isFinite(frame.height) ? Math.max(0, frame.height) : 0;
|
|
3469
|
+
const visible = height > 0;
|
|
3470
|
+
const raw = frame.duration;
|
|
3471
|
+
const duration = raw === void 0 || !Number.isFinite(raw) || raw <= 0 ? FALLBACK_DURATION : Math.min(MAX_DURATION, raw);
|
|
3472
|
+
return {
|
|
3473
|
+
bottom: visible ? height : Math.max(0, safeAreaBottom),
|
|
3474
|
+
keyboardVisible: visible,
|
|
3475
|
+
duration
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
function fitCanvasRect(container, document) {
|
|
3479
|
+
if (container.width <= 0 || container.height <= 0 || document.width <= 0 || document.height <= 0) {
|
|
3480
|
+
return { width: 0, height: 0, left: 0, top: 0 };
|
|
3481
|
+
}
|
|
3482
|
+
const scale = Math.min(container.width / document.width, container.height / document.height);
|
|
3483
|
+
const width = document.width * scale;
|
|
3484
|
+
const height = document.height * scale;
|
|
3485
|
+
return {
|
|
3486
|
+
width,
|
|
3487
|
+
height,
|
|
3488
|
+
left: (container.width - width) / 2,
|
|
3489
|
+
top: (container.height - height) / 2
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
// src/composer/useTextSession.ts
|
|
3494
|
+
import {
|
|
3495
|
+
clampTextSize,
|
|
3496
|
+
collapseTextSpans,
|
|
3497
|
+
createTextDraft,
|
|
3498
|
+
needsSpanCollapse,
|
|
3499
|
+
isBlankText,
|
|
3500
|
+
patchTextSpan,
|
|
3501
|
+
resolveTextSession,
|
|
3502
|
+
textSpanOf,
|
|
3503
|
+
textValueOf,
|
|
3504
|
+
useEditor as useEditor21
|
|
3505
|
+
} from "@pictor-sdk/core";
|
|
3506
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo10, useRef as useRef12, useState as useState9 } from "react";
|
|
3507
|
+
function useTextSession(options = {}) {
|
|
3508
|
+
const { document, dispatch, profile, selection, select } = useEditor21();
|
|
3509
|
+
const { layerId: requestedId, onFinished } = options;
|
|
3510
|
+
const [session] = useState9(
|
|
3511
|
+
() => resolveTextSession(document.layers, requestedId ? [requestedId] : selection)
|
|
3512
|
+
);
|
|
3513
|
+
const [draftId, setDraftId] = useState9(null);
|
|
3514
|
+
const finished = useRef12(false);
|
|
3515
|
+
const priorSelection = useRef12([]);
|
|
3516
|
+
const capturedPrior = useRef12(false);
|
|
3517
|
+
if (!capturedPrior.current) {
|
|
3518
|
+
capturedPrior.current = true;
|
|
3519
|
+
priorSelection.current = [...selection];
|
|
3520
|
+
}
|
|
3521
|
+
const latest = useRef12({ document, dispatch, select, selection, profile });
|
|
3522
|
+
latest.current = { document, dispatch, select, selection, profile };
|
|
3523
|
+
useEffect7(() => {
|
|
3524
|
+
latest.current.dispatch({ type: "history/beginBatch", label: "text" });
|
|
3525
|
+
if (session.kind === "draft") {
|
|
3526
|
+
const draft = createTextDraft(latest.current.profile);
|
|
3527
|
+
setDraftId(draft.id);
|
|
3528
|
+
latest.current.dispatch({ type: "layer/add", layer: draft });
|
|
3529
|
+
latest.current.select([draft.id]);
|
|
3530
|
+
} else if (session.layerId) {
|
|
3531
|
+
const layer2 = latest.current.document.layers.find((l) => l.id === session.layerId);
|
|
3532
|
+
const layerData = layer2?.data;
|
|
3533
|
+
if (layerData && needsSpanCollapse(layerData)) {
|
|
3534
|
+
latest.current.dispatch({
|
|
3535
|
+
type: "layer/patchData",
|
|
3536
|
+
id: session.layerId,
|
|
3537
|
+
patch: { spans: collapseTextSpans(layerData).spans }
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
return () => {
|
|
3542
|
+
if (finished.current) return;
|
|
3543
|
+
finished.current = true;
|
|
3544
|
+
latest.current.dispatch({ type: "history/cancelBatch" });
|
|
3545
|
+
latest.current.select(priorSelection.current);
|
|
3546
|
+
};
|
|
3547
|
+
}, []);
|
|
3548
|
+
const layerId = session.kind === "existing" ? session.layerId : draftId;
|
|
3549
|
+
const layer = layerId ? document.layers.find((l) => l.id === layerId) : void 0;
|
|
3550
|
+
const data = layer ? layer.data : void 0;
|
|
3551
|
+
const span = data ? textSpanOf(data) : void 0;
|
|
3552
|
+
const value = data ? textValueOf(data) : "";
|
|
3553
|
+
const idRef = useRef12(null);
|
|
3554
|
+
idRef.current = layerId ?? null;
|
|
3555
|
+
const dataRef = useRef12(void 0);
|
|
3556
|
+
dataRef.current = data;
|
|
3557
|
+
const patchData = useCallback5((patch) => {
|
|
3558
|
+
const id = idRef.current;
|
|
3559
|
+
if (!id) return;
|
|
3560
|
+
latest.current.dispatch({ type: "layer/patchData", id, patch });
|
|
3561
|
+
}, []);
|
|
3562
|
+
const setValue = useCallback5(
|
|
3563
|
+
(next) => {
|
|
3564
|
+
const current = dataRef.current;
|
|
3565
|
+
if (!current) return;
|
|
3566
|
+
patchData({ spans: patchTextSpan(current, { text: next }).spans });
|
|
3567
|
+
},
|
|
3568
|
+
[patchData]
|
|
3569
|
+
);
|
|
3570
|
+
const styleSpan = useCallback5(
|
|
3571
|
+
(patch) => {
|
|
3572
|
+
const current = dataRef.current;
|
|
3573
|
+
if (!current) return;
|
|
3574
|
+
patchData({ spans: patchTextSpan(current, patch).spans });
|
|
3575
|
+
},
|
|
3576
|
+
[patchData]
|
|
3577
|
+
);
|
|
3578
|
+
const setSize = useCallback5(
|
|
3579
|
+
(size) => styleSpan({ fontSize: clampTextSize(size) }),
|
|
3580
|
+
[styleSpan]
|
|
3581
|
+
);
|
|
3582
|
+
const commit = useCallback5(() => {
|
|
3583
|
+
if (finished.current) return null;
|
|
3584
|
+
finished.current = true;
|
|
3585
|
+
const id = idRef.current;
|
|
3586
|
+
const blank = isBlankText(dataRef.current ? textValueOf(dataRef.current) : "");
|
|
3587
|
+
if (session.kind === "draft" && blank) {
|
|
3588
|
+
latest.current.dispatch({ type: "history/cancelBatch" });
|
|
3589
|
+
latest.current.select(priorSelection.current);
|
|
3590
|
+
onFinished?.(null);
|
|
3591
|
+
return null;
|
|
3592
|
+
}
|
|
3593
|
+
latest.current.dispatch({ type: "history/endBatch" });
|
|
3594
|
+
onFinished?.(id ?? null);
|
|
3595
|
+
return id ?? null;
|
|
3596
|
+
}, [session.kind, onFinished]);
|
|
3597
|
+
const cancel = useCallback5(() => {
|
|
3598
|
+
if (finished.current) return;
|
|
3599
|
+
finished.current = true;
|
|
3600
|
+
latest.current.dispatch({ type: "history/cancelBatch" });
|
|
3601
|
+
latest.current.select(priorSelection.current);
|
|
3602
|
+
onFinished?.(null);
|
|
3603
|
+
}, [onFinished]);
|
|
3604
|
+
return useMemo10(
|
|
3605
|
+
() => ({
|
|
3606
|
+
layer,
|
|
3607
|
+
kind: session.kind,
|
|
3608
|
+
value,
|
|
3609
|
+
setValue,
|
|
3610
|
+
span,
|
|
3611
|
+
data,
|
|
3612
|
+
styleSpan,
|
|
3613
|
+
styleText: patchData,
|
|
3614
|
+
setSize,
|
|
3615
|
+
commit,
|
|
3616
|
+
cancel
|
|
3617
|
+
}),
|
|
3618
|
+
[layer, session.kind, value, setValue, span, data, styleSpan, patchData, setSize, commit, cancel]
|
|
3619
|
+
);
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
// src/composer/TextComposer.tsx
|
|
3623
|
+
import { jsx as jsx27, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
3624
|
+
var SWATCHES2 = ["#ffffff", "#000000", "#ed4956", "#ffe25c", "#78de85", "#4dc4ff", "#a15fed"];
|
|
3625
|
+
var ALIGNS = ["left", "center", "right"];
|
|
3626
|
+
var ALIGN_GLYPH = { left: "\u21E4", center: "\u2630", right: "\u21E5" };
|
|
3627
|
+
var FILL_MODES = [void 0, "solid", "perLine"];
|
|
3628
|
+
var STROKE_WIDTHS = [void 0, 2e-3, 5e-3];
|
|
3629
|
+
var SHADOWS = [
|
|
3630
|
+
void 0,
|
|
3631
|
+
{ color: "#000000", blur: 0.012, dx: 2e-3, dy: 4e-3 },
|
|
3632
|
+
{ color: "#000000", blur: 2e-3, dx: 6e-3, dy: 8e-3 }
|
|
3633
|
+
];
|
|
3634
|
+
function TextComposer({ session, insets, onDone, onCancel }) {
|
|
3635
|
+
const theme = useTheme();
|
|
3636
|
+
const store = useEditorStore5();
|
|
3637
|
+
const { document } = useEditor22();
|
|
3638
|
+
const [container, setContainer] = useState10({ width: 0, height: 0 });
|
|
3639
|
+
const input = useRef13(null);
|
|
3640
|
+
const [selectionRange, setSelectionRange] = useState10(
|
|
3641
|
+
void 0
|
|
3642
|
+
);
|
|
3643
|
+
const data = session.data;
|
|
3644
|
+
const span = session.span;
|
|
3645
|
+
const layout = useMemo11(() => {
|
|
3646
|
+
const canvas = fitCanvasRect(container, document.size);
|
|
3647
|
+
const fontSize = (span?.fontSize ?? 0.08) * canvas.width;
|
|
3648
|
+
const centreY = canvas.top + (session.layer?.transform.y ?? 0.42) * canvas.height;
|
|
3649
|
+
const height = canvas.height;
|
|
3650
|
+
return {
|
|
3651
|
+
fontSize: Math.max(1, fontSize),
|
|
3652
|
+
lineHeight: Math.max(1, fontSize * (span?.lineHeight ?? 1.2)),
|
|
3653
|
+
left: canvas.left,
|
|
3654
|
+
width: Math.max(1, canvas.width),
|
|
3655
|
+
top: Math.max(0, centreY - height / 2),
|
|
3656
|
+
height: Math.max(1, height)
|
|
3657
|
+
};
|
|
3658
|
+
}, [container, document.size, span?.fontSize, span?.lineHeight, session.layer?.transform.y]);
|
|
3659
|
+
const fonts = useMemo11(() => fontChoices(store.registry.fonts), [store.registry.fonts]);
|
|
3660
|
+
useEffect8(() => {
|
|
3661
|
+
const handle = setTimeout(() => input.current?.focus(), 50);
|
|
3662
|
+
return () => clearTimeout(handle);
|
|
3663
|
+
}, []);
|
|
3664
|
+
const onSelectionChange = useCallback6(
|
|
3665
|
+
(event) => {
|
|
3666
|
+
setSelectionRange(event.nativeEvent.selection);
|
|
3667
|
+
},
|
|
3668
|
+
[]
|
|
3669
|
+
);
|
|
3670
|
+
const keepFocus = useCallback6(
|
|
3671
|
+
(apply) => {
|
|
3672
|
+
apply();
|
|
3673
|
+
if (!input.current?.isFocused()) input.current?.focus();
|
|
3674
|
+
if (selectionRange) {
|
|
3675
|
+
const range = selectionRange;
|
|
3676
|
+
requestAnimationFrame(() => input.current?.setNativeProps({ selection: range }));
|
|
3677
|
+
}
|
|
3678
|
+
},
|
|
3679
|
+
[selectionRange]
|
|
3680
|
+
);
|
|
3681
|
+
const cycleAlign = () => {
|
|
3682
|
+
if (!data) return;
|
|
3683
|
+
const next = ALIGNS[(ALIGNS.indexOf(data.align) + 1) % ALIGNS.length];
|
|
3684
|
+
keepFocus(() => session.styleText({ align: next }));
|
|
3685
|
+
};
|
|
3686
|
+
const cycleFill = () => {
|
|
3687
|
+
if (!data || !span) return;
|
|
3688
|
+
const index = FILL_MODES.indexOf(data.fill?.mode);
|
|
3689
|
+
const next = FILL_MODES[(index + 1) % FILL_MODES.length];
|
|
3690
|
+
keepFocus(
|
|
3691
|
+
() => session.styleText({
|
|
3692
|
+
fill: next ? {
|
|
3693
|
+
color: span.color === "#000000" ? "#ffffff" : "#000000",
|
|
3694
|
+
radius: 0.012,
|
|
3695
|
+
padding: 0.012,
|
|
3696
|
+
mode: next
|
|
3697
|
+
} : void 0
|
|
3698
|
+
})
|
|
3699
|
+
);
|
|
3700
|
+
};
|
|
3701
|
+
const cycleStroke = () => {
|
|
3702
|
+
if (!data || !span) return;
|
|
3703
|
+
const current = data.stroke?.width;
|
|
3704
|
+
const index = STROKE_WIDTHS.findIndex((w) => w === current);
|
|
3705
|
+
const next = STROKE_WIDTHS[(index + 1) % STROKE_WIDTHS.length];
|
|
3706
|
+
keepFocus(
|
|
3707
|
+
() => session.styleText({
|
|
3708
|
+
stroke: next ? { color: span.color === "#000000" ? "#ffffff" : "#000000", width: next } : void 0
|
|
3709
|
+
})
|
|
3710
|
+
);
|
|
3711
|
+
};
|
|
3712
|
+
const cycleShadow = () => {
|
|
3713
|
+
if (!data) return;
|
|
3714
|
+
const current = data.shadow;
|
|
3715
|
+
const index = current ? SHADOWS.findIndex((s) => s && current && s.blur === current.blur && s.dx === current.dx) : 0;
|
|
3716
|
+
const next = SHADOWS[(index + 1) % SHADOWS.length];
|
|
3717
|
+
keepFocus(() => session.styleText({ shadow: next ? { ...next } : void 0 }));
|
|
3718
|
+
};
|
|
3719
|
+
useEffect8(() => {
|
|
3720
|
+
const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
|
|
3721
|
+
if (Keyboard.isVisible?.() ?? insets.keyboardVisible) {
|
|
3722
|
+
Keyboard.dismiss();
|
|
3723
|
+
return true;
|
|
3724
|
+
}
|
|
3725
|
+
onCancel();
|
|
3726
|
+
return true;
|
|
3727
|
+
});
|
|
3728
|
+
return () => subscription.remove();
|
|
3729
|
+
}, [onCancel, insets.keyboardVisible]);
|
|
3730
|
+
const controlRow = {
|
|
3731
|
+
flexDirection: "row",
|
|
3732
|
+
alignItems: "center",
|
|
3733
|
+
gap: theme.spacing.sm,
|
|
3734
|
+
paddingHorizontal: theme.spacing.md
|
|
3735
|
+
};
|
|
3736
|
+
return /* @__PURE__ */ jsxs18(
|
|
3737
|
+
View18,
|
|
3738
|
+
{
|
|
3739
|
+
style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0 },
|
|
3740
|
+
onLayout: (event) => {
|
|
3741
|
+
const { width, height } = event.nativeEvent.layout;
|
|
3742
|
+
setContainer(
|
|
3743
|
+
(current) => current.width === width && current.height === height ? current : { width, height }
|
|
3744
|
+
);
|
|
3745
|
+
},
|
|
3746
|
+
children: [
|
|
3747
|
+
/* @__PURE__ */ jsx27(
|
|
3748
|
+
View18,
|
|
3749
|
+
{
|
|
3750
|
+
pointerEvents: "none",
|
|
3751
|
+
style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0, backgroundColor: "#00000055" }
|
|
3752
|
+
}
|
|
3753
|
+
),
|
|
3754
|
+
/* @__PURE__ */ jsx27(
|
|
3755
|
+
TextInput,
|
|
3756
|
+
{
|
|
3757
|
+
ref: input,
|
|
3758
|
+
value: session.value,
|
|
3759
|
+
onChangeText: session.setValue,
|
|
3760
|
+
onSelectionChange,
|
|
3761
|
+
multiline: true,
|
|
3762
|
+
submitBehavior: "newline",
|
|
3763
|
+
autoCorrect: false,
|
|
3764
|
+
spellCheck: false,
|
|
3765
|
+
autoComplete: "off",
|
|
3766
|
+
importantForAutofill: "no",
|
|
3767
|
+
accessibilityLabel: "Story text",
|
|
3768
|
+
placeholder: "",
|
|
3769
|
+
style: {
|
|
3770
|
+
position: "absolute",
|
|
3771
|
+
left: layout.left,
|
|
3772
|
+
width: layout.width,
|
|
3773
|
+
top: layout.top,
|
|
3774
|
+
height: layout.height,
|
|
3775
|
+
// Fully transparent glyphs. The Skia layer is what the user reads;
|
|
3776
|
+
// drawing the same text twice would double it on screen. Written as
|
|
3777
|
+
// rgba rather than the 'transparent' keyword because the keyword is
|
|
3778
|
+
// not reliably honoured for TextInput colour on Android.
|
|
3779
|
+
color: "rgba(0,0,0,0)",
|
|
3780
|
+
fontSize: layout.fontSize,
|
|
3781
|
+
lineHeight: layout.lineHeight,
|
|
3782
|
+
textAlign: data?.align ?? "center",
|
|
3783
|
+
...Platform.OS === "ios" ? {} : { textAlignVertical: "center" },
|
|
3784
|
+
padding: 0
|
|
3785
|
+
},
|
|
3786
|
+
selectionColor: theme.colors.accent,
|
|
3787
|
+
cursorColor: theme.colors.accent
|
|
3788
|
+
}
|
|
3789
|
+
),
|
|
3790
|
+
/* @__PURE__ */ jsxs18(
|
|
3791
|
+
View18,
|
|
3792
|
+
{
|
|
3793
|
+
style: {
|
|
3794
|
+
position: "absolute",
|
|
3795
|
+
left: 0,
|
|
3796
|
+
right: 0,
|
|
3797
|
+
top: 0,
|
|
3798
|
+
height: 56,
|
|
3799
|
+
flexDirection: "row",
|
|
3800
|
+
alignItems: "center",
|
|
3801
|
+
justifyContent: "space-between",
|
|
3802
|
+
paddingHorizontal: theme.spacing.lg
|
|
3803
|
+
},
|
|
3804
|
+
children: [
|
|
3805
|
+
/* @__PURE__ */ jsx27(Pressable15, { accessibilityRole: "button", accessibilityLabel: "Cancel text", onPress: onCancel, children: /* @__PURE__ */ jsx27(Text18, { style: { color: theme.colors.text, fontSize: theme.font.size }, children: "Cancel" }) }),
|
|
3806
|
+
/* @__PURE__ */ jsx27(Pressable15, { accessibilityRole: "button", accessibilityLabel: "Done editing text", onPress: onDone, children: /* @__PURE__ */ jsx27(
|
|
3807
|
+
Text18,
|
|
3808
|
+
{
|
|
3809
|
+
style: {
|
|
3810
|
+
color: theme.colors.accent,
|
|
3811
|
+
fontSize: theme.font.size,
|
|
3812
|
+
fontWeight: theme.font.weight
|
|
3813
|
+
},
|
|
3814
|
+
children: "Done"
|
|
3815
|
+
}
|
|
3816
|
+
) })
|
|
3817
|
+
]
|
|
3818
|
+
}
|
|
3819
|
+
),
|
|
3820
|
+
/* @__PURE__ */ jsxs18(
|
|
3821
|
+
View18,
|
|
3822
|
+
{
|
|
3823
|
+
style: {
|
|
3824
|
+
position: "absolute",
|
|
3825
|
+
left: 0,
|
|
3826
|
+
right: 0,
|
|
3827
|
+
bottom: insets.bottom,
|
|
3828
|
+
paddingVertical: theme.spacing.sm,
|
|
3829
|
+
gap: theme.spacing.sm,
|
|
3830
|
+
backgroundColor: "#00000066"
|
|
3831
|
+
},
|
|
3832
|
+
children: [
|
|
3833
|
+
/* @__PURE__ */ jsxs18(View18, { style: controlRow, children: [
|
|
3834
|
+
/* @__PURE__ */ jsx27(
|
|
3835
|
+
Text18,
|
|
3836
|
+
{
|
|
3837
|
+
style: { color: theme.colors.textMuted, fontSize: theme.font.sizeSm, width: 34 },
|
|
3838
|
+
accessibilityLabel: "Text size",
|
|
3839
|
+
children: "Aa"
|
|
3840
|
+
}
|
|
3841
|
+
),
|
|
3842
|
+
/* @__PURE__ */ jsx27(View18, { style: { flex: 1 }, children: /* @__PURE__ */ jsx27(
|
|
3843
|
+
Slider,
|
|
3844
|
+
{
|
|
3845
|
+
value: span?.fontSize ?? MIN_TEXT_SIZE,
|
|
3846
|
+
min: MIN_TEXT_SIZE,
|
|
3847
|
+
max: MAX_TEXT_SIZE,
|
|
3848
|
+
centered: false,
|
|
3849
|
+
onChange: (size) => session.setSize(size)
|
|
3850
|
+
}
|
|
3851
|
+
) }),
|
|
3852
|
+
/* @__PURE__ */ jsx27(
|
|
3853
|
+
Pressable15,
|
|
3854
|
+
{
|
|
3855
|
+
accessibilityRole: "button",
|
|
3856
|
+
accessibilityLabel: `Alignment ${data?.align ?? "center"}`,
|
|
3857
|
+
onPress: cycleAlign,
|
|
3858
|
+
style: { padding: theme.spacing.sm },
|
|
3859
|
+
children: /* @__PURE__ */ jsx27(Text18, { style: { color: theme.colors.text, fontWeight: theme.font.weight }, children: ALIGN_GLYPH[data?.align ?? "center"] })
|
|
3860
|
+
}
|
|
3861
|
+
),
|
|
3862
|
+
/* @__PURE__ */ jsx27(
|
|
3863
|
+
Pressable15,
|
|
3864
|
+
{
|
|
3865
|
+
accessibilityRole: "button",
|
|
3866
|
+
accessibilityLabel: "Text background",
|
|
3867
|
+
accessibilityState: { selected: Boolean(data?.fill) },
|
|
3868
|
+
onPress: cycleFill,
|
|
3869
|
+
style: { padding: theme.spacing.sm },
|
|
3870
|
+
children: /* @__PURE__ */ jsx27(
|
|
3871
|
+
Text18,
|
|
3872
|
+
{
|
|
3873
|
+
style: {
|
|
3874
|
+
color: data?.fill ? theme.colors.accent : theme.colors.text,
|
|
3875
|
+
fontWeight: theme.font.weight
|
|
3876
|
+
},
|
|
3877
|
+
children: data?.fill?.mode === "perLine" ? "A\u20DE\u20DE" : "A\u20DE"
|
|
3878
|
+
}
|
|
3879
|
+
)
|
|
3880
|
+
}
|
|
3881
|
+
),
|
|
3882
|
+
/* @__PURE__ */ jsx27(
|
|
3883
|
+
Pressable15,
|
|
3884
|
+
{
|
|
3885
|
+
accessibilityRole: "button",
|
|
3886
|
+
accessibilityLabel: "Text outline",
|
|
3887
|
+
accessibilityState: { selected: Boolean(data?.stroke) },
|
|
3888
|
+
onPress: cycleStroke,
|
|
3889
|
+
style: { padding: theme.spacing.sm },
|
|
3890
|
+
children: /* @__PURE__ */ jsx27(
|
|
3891
|
+
Text18,
|
|
3892
|
+
{
|
|
3893
|
+
style: {
|
|
3894
|
+
color: data?.stroke ? theme.colors.accent : theme.colors.text,
|
|
3895
|
+
fontWeight: theme.font.weight
|
|
3896
|
+
},
|
|
3897
|
+
children: "\u24B6"
|
|
3898
|
+
}
|
|
3899
|
+
)
|
|
3900
|
+
}
|
|
3901
|
+
),
|
|
3902
|
+
/* @__PURE__ */ jsx27(
|
|
3903
|
+
Pressable15,
|
|
3904
|
+
{
|
|
3905
|
+
accessibilityRole: "button",
|
|
3906
|
+
accessibilityLabel: "Text shadow",
|
|
3907
|
+
accessibilityState: { selected: Boolean(data?.shadow) },
|
|
3908
|
+
onPress: cycleShadow,
|
|
3909
|
+
style: { padding: theme.spacing.sm },
|
|
3910
|
+
children: /* @__PURE__ */ jsx27(
|
|
3911
|
+
Text18,
|
|
3912
|
+
{
|
|
3913
|
+
style: {
|
|
3914
|
+
color: data?.shadow ? theme.colors.accent : theme.colors.text,
|
|
3915
|
+
fontWeight: theme.font.weight
|
|
3916
|
+
},
|
|
3917
|
+
children: "A\u02D9"
|
|
3918
|
+
}
|
|
3919
|
+
)
|
|
3920
|
+
}
|
|
3921
|
+
)
|
|
3922
|
+
] }),
|
|
3923
|
+
/* @__PURE__ */ jsx27(
|
|
3924
|
+
ScrollView9,
|
|
3925
|
+
{
|
|
3926
|
+
horizontal: true,
|
|
3927
|
+
keyboardShouldPersistTaps: "always",
|
|
3928
|
+
showsHorizontalScrollIndicator: false,
|
|
3929
|
+
contentContainerStyle: { gap: theme.spacing.sm, paddingHorizontal: theme.spacing.md, alignItems: "center" },
|
|
3930
|
+
style: { flexGrow: 0 },
|
|
3931
|
+
children: fonts.map((font) => /* @__PURE__ */ jsx27(
|
|
3932
|
+
Pressable15,
|
|
3933
|
+
{
|
|
3934
|
+
accessibilityRole: "button",
|
|
3935
|
+
accessibilityLabel: `Font ${font.label}`,
|
|
3936
|
+
accessibilityState: { selected: span?.fontFamily === font.family },
|
|
3937
|
+
onPress: () => keepFocus(() => session.styleSpan({ fontFamily: font.family })),
|
|
3938
|
+
style: {
|
|
3939
|
+
paddingHorizontal: theme.spacing.md,
|
|
3940
|
+
paddingVertical: theme.spacing.xs,
|
|
3941
|
+
borderRadius: theme.radii.md,
|
|
3942
|
+
backgroundColor: span?.fontFamily === font.family ? theme.colors.surface : "transparent",
|
|
3943
|
+
borderWidth: 1,
|
|
3944
|
+
borderColor: span?.fontFamily === font.family ? theme.colors.accent : theme.colors.track
|
|
3945
|
+
},
|
|
3946
|
+
children: /* @__PURE__ */ jsx27(Text18, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: font.label })
|
|
3947
|
+
},
|
|
3948
|
+
font.family
|
|
3949
|
+
))
|
|
3950
|
+
}
|
|
3951
|
+
),
|
|
3952
|
+
/* @__PURE__ */ jsx27(
|
|
3953
|
+
ScrollView9,
|
|
3954
|
+
{
|
|
3955
|
+
horizontal: true,
|
|
3956
|
+
keyboardShouldPersistTaps: "always",
|
|
3957
|
+
showsHorizontalScrollIndicator: false,
|
|
3958
|
+
contentContainerStyle: { gap: theme.spacing.sm, paddingHorizontal: theme.spacing.md, alignItems: "center" },
|
|
3959
|
+
style: { flexGrow: 0 },
|
|
3960
|
+
children: SWATCHES2.map((color) => /* @__PURE__ */ jsx27(
|
|
3961
|
+
Pressable15,
|
|
3962
|
+
{
|
|
3963
|
+
accessibilityRole: "button",
|
|
3964
|
+
accessibilityLabel: `Text colour ${color}`,
|
|
3965
|
+
accessibilityState: { selected: span?.color === color },
|
|
3966
|
+
onPress: () => keepFocus(() => session.styleSpan({ color })),
|
|
3967
|
+
style: {
|
|
3968
|
+
width: 28,
|
|
3969
|
+
height: 28,
|
|
3970
|
+
borderRadius: 14,
|
|
3971
|
+
backgroundColor: color,
|
|
3972
|
+
borderWidth: span?.color === color ? 3 : 1,
|
|
3973
|
+
borderColor: span?.color === color ? theme.colors.accent : theme.colors.track
|
|
3974
|
+
}
|
|
3975
|
+
},
|
|
3976
|
+
color
|
|
3977
|
+
))
|
|
3978
|
+
}
|
|
3979
|
+
)
|
|
3980
|
+
]
|
|
3981
|
+
}
|
|
3982
|
+
)
|
|
3983
|
+
]
|
|
3984
|
+
}
|
|
3985
|
+
);
|
|
3986
|
+
}
|
|
3987
|
+
function TextComposerHost({
|
|
3988
|
+
Composer = TextComposer
|
|
3989
|
+
}) {
|
|
3990
|
+
const { setActiveTool } = useEditor22();
|
|
3991
|
+
const session = useTextSession();
|
|
3992
|
+
const [frame, setFrame] = useState10(HIDDEN_KEYBOARD);
|
|
3993
|
+
const token = useRef13(0);
|
|
3994
|
+
useEffect8(() => {
|
|
3995
|
+
token.current += 1;
|
|
3996
|
+
const mine = token.current;
|
|
3997
|
+
const show = Keyboard.addListener(
|
|
3998
|
+
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow",
|
|
3999
|
+
(event) => {
|
|
4000
|
+
if (mine !== token.current) return;
|
|
4001
|
+
setFrame({ height: event.endCoordinates?.height ?? 0, duration: event.duration });
|
|
4002
|
+
}
|
|
4003
|
+
);
|
|
4004
|
+
const hide = Keyboard.addListener(
|
|
4005
|
+
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide",
|
|
4006
|
+
(event) => {
|
|
4007
|
+
if (mine !== token.current) return;
|
|
4008
|
+
setFrame({ height: 0, duration: event?.duration });
|
|
4009
|
+
}
|
|
4010
|
+
);
|
|
4011
|
+
return () => {
|
|
4012
|
+
token.current += 1;
|
|
4013
|
+
show.remove();
|
|
4014
|
+
hide.remove();
|
|
4015
|
+
};
|
|
4016
|
+
}, []);
|
|
4017
|
+
const insets = useMemo11(() => composerInsets(frame), [frame]);
|
|
4018
|
+
const finish = useCallback6(
|
|
4019
|
+
(commit) => {
|
|
4020
|
+
if (commit) session.commit();
|
|
4021
|
+
else session.cancel();
|
|
4022
|
+
Keyboard.dismiss();
|
|
4023
|
+
setActiveTool(null);
|
|
4024
|
+
},
|
|
4025
|
+
[session, setActiveTool]
|
|
4026
|
+
);
|
|
4027
|
+
return /* @__PURE__ */ jsx27(
|
|
4028
|
+
Composer,
|
|
4029
|
+
{
|
|
4030
|
+
session,
|
|
4031
|
+
insets,
|
|
4032
|
+
onDone: () => finish(true),
|
|
4033
|
+
onCancel: () => finish(false)
|
|
4034
|
+
}
|
|
4035
|
+
);
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
// src/components/TopBar.tsx
|
|
4039
|
+
import { useEditor as useEditor23 } from "@pictor-sdk/core";
|
|
4040
|
+
import { ActivityIndicator, Pressable as Pressable16, Text as Text19, View as View19 } from "react-native";
|
|
4041
|
+
import { jsx as jsx28, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
4042
|
+
function TopBar({ onCancel, onNext, busy = false }) {
|
|
4043
|
+
const { canUndo, canRedo, dispatch } = useEditor23();
|
|
4044
|
+
const theme = useTheme();
|
|
4045
|
+
const button = (label, enabled, onPress, color) => /* @__PURE__ */ jsx28(Pressable16, { disabled: !enabled, onPress, style: { padding: theme.spacing.sm }, children: /* @__PURE__ */ jsx28(
|
|
4046
|
+
Text19,
|
|
4047
|
+
{
|
|
4048
|
+
style: {
|
|
4049
|
+
color: enabled ? color ?? theme.colors.text : theme.colors.textMuted,
|
|
4050
|
+
fontSize: theme.font.size,
|
|
4051
|
+
fontWeight: theme.font.weight
|
|
4052
|
+
},
|
|
4053
|
+
children: label
|
|
4054
|
+
}
|
|
4055
|
+
) });
|
|
4056
|
+
return /* @__PURE__ */ jsxs19(
|
|
4057
|
+
View19,
|
|
4058
|
+
{
|
|
4059
|
+
style: {
|
|
4060
|
+
flexDirection: "row",
|
|
4061
|
+
alignItems: "center",
|
|
4062
|
+
justifyContent: "space-between",
|
|
4063
|
+
paddingHorizontal: theme.spacing.sm,
|
|
4064
|
+
backgroundColor: theme.colors.background
|
|
4065
|
+
},
|
|
4066
|
+
children: [
|
|
4067
|
+
button("Cancel", !!onCancel, onCancel),
|
|
4068
|
+
/* @__PURE__ */ jsxs19(View19, { style: { flexDirection: "row" }, children: [
|
|
4069
|
+
button("\u21BA", canUndo, () => dispatch({ type: "history/undo" })),
|
|
4070
|
+
button("\u21BB", canRedo, () => dispatch({ type: "history/redo" }))
|
|
4071
|
+
] }),
|
|
4072
|
+
busy ? /* @__PURE__ */ jsx28(ActivityIndicator, { color: theme.colors.accent, style: { padding: theme.spacing.sm } }) : button("Next", !!onNext, onNext, theme.colors.accent)
|
|
4073
|
+
]
|
|
4074
|
+
}
|
|
4075
|
+
);
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
// src/components/ToolRail.tsx
|
|
4079
|
+
import { useEditor as useEditor24, useTools as useTools2 } from "@pictor-sdk/core";
|
|
4080
|
+
import { Pressable as Pressable17, ScrollView as ScrollView10, Text as Text20, View as View20 } from "react-native";
|
|
4081
|
+
import { jsx as jsx29, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
4082
|
+
function ToolRail() {
|
|
4083
|
+
const { activeTool, setActiveTool, document, selection, dispatch } = useEditor24();
|
|
4084
|
+
const tools = useTools2();
|
|
4085
|
+
const theme = useTheme();
|
|
4086
|
+
return /* @__PURE__ */ jsx29(
|
|
4087
|
+
ScrollView10,
|
|
4088
|
+
{
|
|
4089
|
+
horizontal: true,
|
|
4090
|
+
showsHorizontalScrollIndicator: false,
|
|
4091
|
+
contentContainerStyle: {
|
|
4092
|
+
gap: theme.spacing.md,
|
|
4093
|
+
paddingHorizontal: theme.spacing.md,
|
|
4094
|
+
paddingVertical: theme.spacing.md
|
|
4095
|
+
},
|
|
4096
|
+
style: { flexGrow: 0, backgroundColor: theme.colors.background },
|
|
4097
|
+
children: tools.map((tool) => {
|
|
4098
|
+
const enabled = tool.isEnabled ? tool.isEnabled({ document, selection }) : true;
|
|
4099
|
+
const active = activeTool === tool.id;
|
|
4100
|
+
return /* @__PURE__ */ jsxs20(
|
|
4101
|
+
Pressable17,
|
|
4102
|
+
{
|
|
4103
|
+
disabled: !enabled,
|
|
4104
|
+
onPress: () => {
|
|
4105
|
+
if (tool.onActivate) {
|
|
4106
|
+
tool.onActivate({ document, dispatch });
|
|
4107
|
+
} else {
|
|
4108
|
+
setActiveTool(active ? null : tool.id);
|
|
4109
|
+
}
|
|
4110
|
+
},
|
|
4111
|
+
style: { width: 72, alignItems: "center", gap: 6, opacity: enabled ? 1 : 0.4 },
|
|
4112
|
+
children: [
|
|
4113
|
+
/* @__PURE__ */ jsx29(View20, { style: { width: 46, height: 46, borderRadius: 23, borderWidth: 1, borderColor: active ? theme.colors.accent : theme.colors.textMuted, backgroundColor: active ? theme.colors.surface : "transparent", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx29(Text20, { style: { color: active ? theme.colors.accent : theme.colors.text, fontSize: 18 }, children: tool.label.slice(0, 1) }) }),
|
|
4114
|
+
/* @__PURE__ */ jsx29(
|
|
4115
|
+
Text20,
|
|
4116
|
+
{
|
|
4117
|
+
style: {
|
|
4118
|
+
color: active ? theme.colors.accent : theme.colors.text,
|
|
4119
|
+
fontSize: theme.font.sizeSm,
|
|
4120
|
+
fontWeight: theme.font.weight
|
|
4121
|
+
},
|
|
4122
|
+
numberOfLines: 1,
|
|
4123
|
+
children: tool.label
|
|
4124
|
+
}
|
|
4125
|
+
)
|
|
4126
|
+
]
|
|
4127
|
+
},
|
|
4128
|
+
tool.id
|
|
4129
|
+
);
|
|
4130
|
+
})
|
|
4131
|
+
}
|
|
4132
|
+
);
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
// src/Editor.tsx
|
|
4136
|
+
import { Fragment, jsx as jsx30, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
4137
|
+
function CanvasStage({ children, onError }) {
|
|
4138
|
+
const { activeTool, profile } = useEditor25();
|
|
4139
|
+
const { previews } = useInteractionState();
|
|
4140
|
+
const surface = resolveGestureSurface(activeTool);
|
|
4141
|
+
return /* @__PURE__ */ jsxs21(
|
|
4142
|
+
EditorCanvas,
|
|
4143
|
+
{
|
|
4144
|
+
style: { flex: 1 },
|
|
4145
|
+
onError,
|
|
4146
|
+
previewTransforms: previews,
|
|
4147
|
+
showSelectionChrome: selectionChromeEnabled(profile),
|
|
4148
|
+
children: [
|
|
4149
|
+
surface === "draw" ? /* @__PURE__ */ jsx30(DrawingSurface, {}) : surface === "crop" ? /* @__PURE__ */ jsxs21(Fragment, { children: [
|
|
4150
|
+
/* @__PURE__ */ jsx30(CropGestureSurface, {}),
|
|
4151
|
+
/* @__PURE__ */ jsx30(CropGrid, {})
|
|
4152
|
+
] }) : surface === "tilt" ? /* @__PURE__ */ jsx30(TiltShiftGestureSurface, {}) : surface === "media" ? /* @__PURE__ */ jsx30(StoryMediaGestureSurface, {}) : /* @__PURE__ */ jsxs21(Fragment, { children: [
|
|
4153
|
+
/* @__PURE__ */ jsx30(LayerGestureSurface, {}),
|
|
4154
|
+
/* @__PURE__ */ jsx30(InteractionGuides, {})
|
|
4155
|
+
] }),
|
|
4156
|
+
children
|
|
4157
|
+
]
|
|
4158
|
+
}
|
|
4159
|
+
);
|
|
4160
|
+
}
|
|
4161
|
+
function mergeSlots(overrides) {
|
|
4162
|
+
return {
|
|
4163
|
+
TopBar,
|
|
4164
|
+
CanvasStage,
|
|
4165
|
+
ToolRail,
|
|
4166
|
+
ToolPanelHost,
|
|
4167
|
+
TextComposer,
|
|
4168
|
+
...overrides
|
|
4169
|
+
};
|
|
4170
|
+
}
|
|
4171
|
+
function EditorBody({
|
|
4172
|
+
slots,
|
|
4173
|
+
onExport,
|
|
4174
|
+
onCancel,
|
|
4175
|
+
onError,
|
|
4176
|
+
exportSpec,
|
|
4177
|
+
stickers
|
|
4178
|
+
}) {
|
|
4179
|
+
const { export: exportDoc, activeTool } = useEditor25();
|
|
4180
|
+
const { layerDragActive } = useInteractionState();
|
|
4181
|
+
const [, setStickers] = useUiState("stickers", []);
|
|
4182
|
+
React19.useEffect(() => {
|
|
4183
|
+
if (stickers) setStickers(stickers);
|
|
4184
|
+
}, [stickers, setStickers]);
|
|
4185
|
+
const theme = useTheme();
|
|
4186
|
+
const [busy, setBusy] = useState11(false);
|
|
4187
|
+
const handleNext = onExport ? async () => {
|
|
4188
|
+
setBusy(true);
|
|
4189
|
+
try {
|
|
4190
|
+
const bytes = await exportDoc(exportSpec);
|
|
4191
|
+
await onExport(bytes);
|
|
4192
|
+
} catch (error) {
|
|
4193
|
+
onError?.(error);
|
|
4194
|
+
} finally {
|
|
4195
|
+
setBusy(false);
|
|
4196
|
+
}
|
|
4197
|
+
} : void 0;
|
|
4198
|
+
const {
|
|
4199
|
+
TopBar: Top,
|
|
4200
|
+
CanvasStage: Stage,
|
|
4201
|
+
ToolRail: Rail,
|
|
4202
|
+
ToolPanelHost: PanelHost,
|
|
4203
|
+
TextComposer: Composer
|
|
4204
|
+
} = slots;
|
|
4205
|
+
const composing = activeTool === "text" && Composer !== null;
|
|
4206
|
+
const chrome = !layerDragActive;
|
|
4207
|
+
const hidden = { opacity: chrome ? 1 : 0 };
|
|
4208
|
+
const hits = chrome ? "auto" : "none";
|
|
4209
|
+
return (
|
|
4210
|
+
// GestureDetector requires a GestureHandlerRootView ancestor; providing it
|
|
4211
|
+
// here means consumers never have to know (nesting under an app-level one
|
|
4212
|
+
// is harmless).
|
|
4213
|
+
/* @__PURE__ */ jsx30(GestureHandlerRootView, { style: { flex: 1 }, children: /* @__PURE__ */ jsxs21(View21, { style: { flex: 1, backgroundColor: theme.colors.background }, children: [
|
|
4214
|
+
Top && !composing ? /* @__PURE__ */ jsx30(View21, { style: hidden, pointerEvents: hits, children: /* @__PURE__ */ jsx30(Top, { onCancel, onNext: handleNext, busy }) }) : null,
|
|
4215
|
+
Stage ? /* @__PURE__ */ jsx30(Stage, { onError }) : null,
|
|
4216
|
+
PanelHost && !composing ? /* @__PURE__ */ jsx30(View21, { style: hidden, pointerEvents: hits, children: /* @__PURE__ */ jsx30(PanelHost, {}) }) : null,
|
|
4217
|
+
Rail && !activeTool ? /* @__PURE__ */ jsx30(View21, { style: hidden, pointerEvents: hits, children: /* @__PURE__ */ jsx30(Rail, {}) }) : null,
|
|
4218
|
+
composing && Composer ? /* @__PURE__ */ jsx30(TextComposerHost, { Composer }) : null
|
|
4219
|
+
] }) })
|
|
4220
|
+
);
|
|
4221
|
+
}
|
|
4222
|
+
function Editor({
|
|
4223
|
+
profile,
|
|
4224
|
+
plugins,
|
|
4225
|
+
source,
|
|
4226
|
+
intrinsic,
|
|
4227
|
+
initialDocument,
|
|
4228
|
+
onExport,
|
|
4229
|
+
onCancel,
|
|
4230
|
+
onError,
|
|
4231
|
+
exportSpec,
|
|
4232
|
+
slots,
|
|
4233
|
+
theme,
|
|
4234
|
+
stickers,
|
|
4235
|
+
requestImages
|
|
4236
|
+
}) {
|
|
4237
|
+
return /* @__PURE__ */ jsx30(
|
|
4238
|
+
EditorProvider,
|
|
4239
|
+
{
|
|
4240
|
+
profile,
|
|
4241
|
+
plugins,
|
|
4242
|
+
source,
|
|
4243
|
+
intrinsic,
|
|
4244
|
+
initialDocument,
|
|
4245
|
+
children: /* @__PURE__ */ jsx30(ThemeContext.Provider, { value: mergeTheme(theme), children: /* @__PURE__ */ jsx30(UiStateProvider, { children: /* @__PURE__ */ jsx30(HostCallbacksProvider, { value: { requestImages, onError }, children: /* @__PURE__ */ jsx30(InteractionProvider, { children: /* @__PURE__ */ jsx30(
|
|
4246
|
+
EditorBody,
|
|
4247
|
+
{
|
|
4248
|
+
slots: mergeSlots(slots),
|
|
4249
|
+
onExport,
|
|
4250
|
+
onCancel,
|
|
4251
|
+
onError,
|
|
4252
|
+
exportSpec,
|
|
4253
|
+
stickers
|
|
4254
|
+
}
|
|
4255
|
+
) }) }) }) })
|
|
4256
|
+
}
|
|
4257
|
+
);
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
// src/gestures/EditorGestures.tsx
|
|
4261
|
+
import { useEditorStore as useEditorStore6 } from "@pictor-sdk/core";
|
|
4262
|
+
import { useCanvasMetrics as useCanvasMetrics7 } from "@pictor-sdk/skia";
|
|
4263
|
+
import { useMemo as useMemo12, useRef as useRef14 } from "react";
|
|
4264
|
+
import { StyleSheet as StyleSheet8 } from "react-native";
|
|
4265
|
+
import { Gesture as Gesture8, GestureDetector as GestureDetector8 } from "react-native-gesture-handler";
|
|
4266
|
+
import Animated8 from "react-native-reanimated";
|
|
4267
|
+
import { runOnJS as runOnJS8 } from "react-native-worklets";
|
|
4268
|
+
|
|
4269
|
+
// src/gestures/controller.ts
|
|
4270
|
+
import {
|
|
4271
|
+
hitTestDocument as hitTestDocument2,
|
|
4272
|
+
snapCenter,
|
|
4273
|
+
snapRotation
|
|
4274
|
+
} from "@pictor-sdk/core";
|
|
4275
|
+
function createGestureController(store, getMetrics, options = {}) {
|
|
4276
|
+
let active = 0;
|
|
4277
|
+
let base = null;
|
|
4278
|
+
let layerId = null;
|
|
4279
|
+
let pan = { x: 0, y: 0 };
|
|
4280
|
+
let pinch = 1;
|
|
4281
|
+
let rotate = 0;
|
|
4282
|
+
const snapState = { rotation: false, centerX: false, centerY: false };
|
|
4283
|
+
function targetLayerId() {
|
|
4284
|
+
return store.getState().selection[0] ?? null;
|
|
4285
|
+
}
|
|
4286
|
+
function begin() {
|
|
4287
|
+
if (active === 0) {
|
|
4288
|
+
layerId = targetLayerId();
|
|
4289
|
+
if (!layerId) return;
|
|
4290
|
+
const layer = store.getState().document.layers.find((l) => l.id === layerId);
|
|
4291
|
+
if (!layer || layer.locked) {
|
|
4292
|
+
layerId = null;
|
|
4293
|
+
return;
|
|
4294
|
+
}
|
|
4295
|
+
base = { ...layer.transform };
|
|
4296
|
+
pan = { x: 0, y: 0 };
|
|
4297
|
+
pinch = 1;
|
|
4298
|
+
rotate = 0;
|
|
4299
|
+
snapState.rotation = snapState.centerX = snapState.centerY = false;
|
|
4300
|
+
store.dispatch({ type: "history/beginBatch", label: "gesture" });
|
|
4301
|
+
}
|
|
4302
|
+
active++;
|
|
4303
|
+
}
|
|
4304
|
+
function fireSnap(kind, snapped) {
|
|
4305
|
+
if (snapped && !snapState[kind]) options.onSnap?.({ kind });
|
|
4306
|
+
snapState[kind] = snapped;
|
|
4307
|
+
}
|
|
4308
|
+
function commit() {
|
|
4309
|
+
if (!base || !layerId) return;
|
|
4310
|
+
const metrics = getMetrics();
|
|
4311
|
+
if (!metrics || metrics.width === 0 || metrics.height === 0) return;
|
|
4312
|
+
const rawX = base.x + pan.x / metrics.width;
|
|
4313
|
+
const rawY = base.y + pan.y / metrics.height;
|
|
4314
|
+
const rawRotation = base.rotation + rotate;
|
|
4315
|
+
const sx = snapCenter(rawX);
|
|
4316
|
+
const sy = snapCenter(rawY);
|
|
4317
|
+
const sr = snapRotation(rawRotation);
|
|
4318
|
+
fireSnap("centerX", sx.snapped);
|
|
4319
|
+
fireSnap("centerY", sy.snapped);
|
|
4320
|
+
fireSnap("rotation", sr.snapped);
|
|
4321
|
+
store.dispatch({
|
|
4322
|
+
type: "layer/transform",
|
|
4323
|
+
id: layerId,
|
|
4324
|
+
patch: {
|
|
4325
|
+
x: sx.value,
|
|
4326
|
+
y: sy.value,
|
|
4327
|
+
rotation: sr.value,
|
|
4328
|
+
scale: Math.max(0.01, base.scale * pinch)
|
|
4329
|
+
}
|
|
4330
|
+
});
|
|
4331
|
+
}
|
|
4332
|
+
function end() {
|
|
4333
|
+
if (active === 0) return;
|
|
4334
|
+
active--;
|
|
4335
|
+
if (active === 0) {
|
|
4336
|
+
if (layerId) store.dispatch({ type: "history/endBatch" });
|
|
4337
|
+
base = null;
|
|
4338
|
+
layerId = null;
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
return {
|
|
4342
|
+
/** Pan/pinch/rotate lifecycles — call begin/end once per recognizer. */
|
|
4343
|
+
begin,
|
|
4344
|
+
end,
|
|
4345
|
+
setPan(xDp, yDp) {
|
|
4346
|
+
pan = { x: xDp, y: yDp };
|
|
4347
|
+
commit();
|
|
4348
|
+
},
|
|
4349
|
+
setPinch(scale) {
|
|
4350
|
+
pinch = scale;
|
|
4351
|
+
commit();
|
|
4352
|
+
},
|
|
4353
|
+
setRotate(rad) {
|
|
4354
|
+
rotate = rad;
|
|
4355
|
+
commit();
|
|
4356
|
+
},
|
|
4357
|
+
/** Tap in canvas dp → hit-test → select (empty tap clears selection). */
|
|
4358
|
+
tap(xDp, yDp) {
|
|
4359
|
+
const metrics = getMetrics();
|
|
4360
|
+
if (!metrics) return;
|
|
4361
|
+
const point = { x: xDp / metrics.width, y: yDp / metrics.height };
|
|
4362
|
+
const hit = hitTestDocument2(store.getState().document, point, store.registry);
|
|
4363
|
+
store.select(hit ? [hit.id] : []);
|
|
4364
|
+
},
|
|
4365
|
+
/** true while a gesture is in flight (used to mute other UI). */
|
|
4366
|
+
isActive: () => active > 0
|
|
4367
|
+
};
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
// src/gestures/EditorGestures.tsx
|
|
4371
|
+
import { jsx as jsx31 } from "react/jsx-runtime";
|
|
4372
|
+
function EditorGestures({ onSnap }) {
|
|
4373
|
+
const store = useEditorStore6();
|
|
4374
|
+
const metrics = useCanvasMetrics7();
|
|
4375
|
+
const metricsRef = useRef14(metrics);
|
|
4376
|
+
metricsRef.current = metrics;
|
|
4377
|
+
const onSnapRef = useRef14(onSnap);
|
|
4378
|
+
onSnapRef.current = onSnap;
|
|
4379
|
+
const controller = useMemo12(
|
|
4380
|
+
() => createGestureController(store, () => metricsRef.current, {
|
|
4381
|
+
onSnap: (e) => onSnapRef.current?.(e)
|
|
4382
|
+
}),
|
|
4383
|
+
// metricsRef/onSnap are read through refs — store identity is the only dep
|
|
4384
|
+
[store]
|
|
4385
|
+
);
|
|
4386
|
+
const gesture = useMemo12(() => {
|
|
4387
|
+
const tap = Gesture8.Tap().onEnd((e) => {
|
|
4388
|
+
"worklet";
|
|
4389
|
+
runOnJS8(controller.tap)(e.x, e.y);
|
|
4390
|
+
});
|
|
4391
|
+
const pan = Gesture8.Pan().minPointers(1).onStart(() => {
|
|
4392
|
+
"worklet";
|
|
4393
|
+
runOnJS8(controller.begin)();
|
|
4394
|
+
}).onUpdate((e) => {
|
|
4395
|
+
"worklet";
|
|
4396
|
+
runOnJS8(controller.setPan)(e.translationX, e.translationY);
|
|
4397
|
+
}).onFinalize(() => {
|
|
4398
|
+
"worklet";
|
|
4399
|
+
runOnJS8(controller.end)();
|
|
4400
|
+
});
|
|
4401
|
+
const pinch = Gesture8.Pinch().onStart(() => {
|
|
4402
|
+
"worklet";
|
|
4403
|
+
runOnJS8(controller.begin)();
|
|
4404
|
+
}).onUpdate((e) => {
|
|
4405
|
+
"worklet";
|
|
4406
|
+
runOnJS8(controller.setPinch)(e.scale);
|
|
4407
|
+
}).onFinalize(() => {
|
|
4408
|
+
"worklet";
|
|
4409
|
+
runOnJS8(controller.end)();
|
|
4410
|
+
});
|
|
4411
|
+
const rotate = Gesture8.Rotation().onStart(() => {
|
|
4412
|
+
"worklet";
|
|
4413
|
+
runOnJS8(controller.begin)();
|
|
4414
|
+
}).onUpdate((e) => {
|
|
4415
|
+
"worklet";
|
|
4416
|
+
runOnJS8(controller.setRotate)(e.rotation);
|
|
4417
|
+
}).onFinalize(() => {
|
|
4418
|
+
"worklet";
|
|
4419
|
+
runOnJS8(controller.end)();
|
|
4420
|
+
});
|
|
4421
|
+
return Gesture8.Race(
|
|
4422
|
+
Gesture8.Simultaneous(pan, pinch, rotate),
|
|
4423
|
+
tap
|
|
4424
|
+
);
|
|
4425
|
+
}, [controller]);
|
|
4426
|
+
return /* @__PURE__ */ jsx31(GestureDetector8, { gesture, children: /* @__PURE__ */ jsx31(Animated8.View, { style: StyleSheet8.absoluteFill }) });
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4429
|
+
// src/index.ts
|
|
4430
|
+
import { POST_PROFILE, STORY_PROFILE } from "@pictor-sdk/core";
|
|
4431
|
+
|
|
4432
|
+
// src/panels/TextPanel.tsx
|
|
4433
|
+
import {
|
|
4434
|
+
createId as createId3,
|
|
4435
|
+
useEditor as useEditor26,
|
|
4436
|
+
useEditorStore as useEditorStore7
|
|
4437
|
+
} from "@pictor-sdk/core";
|
|
4438
|
+
import { Pressable as Pressable18, ScrollView as ScrollView11, Text as Text21, TextInput as TextInput2, View as View22 } from "react-native";
|
|
4439
|
+
import { jsx as jsx32, jsxs as jsxs22 } from "react/jsx-runtime";
|
|
4440
|
+
var SWATCHES3 = ["#ffffff", "#000000", "#ed4956", "#ffe25c", "#78de85", "#4dc4ff", "#a15fed"];
|
|
4441
|
+
var ALIGNS2 = ["left", "center", "right"];
|
|
4442
|
+
var FILL_MODES2 = [void 0, "solid", "perLine"];
|
|
4443
|
+
function TextPanel() {
|
|
4444
|
+
const { document, selection, dispatch, select } = useEditor26();
|
|
4445
|
+
const store = useEditorStore7();
|
|
4446
|
+
const theme = useTheme();
|
|
4447
|
+
const selected = document.layers.find(
|
|
4448
|
+
(l) => l.type === "text" && selection.includes(l.id)
|
|
4449
|
+
);
|
|
4450
|
+
const data = selected ? selected.data : null;
|
|
4451
|
+
const span = data?.spans[0] ?? null;
|
|
4452
|
+
const addText = () => {
|
|
4453
|
+
const id = createId3("text");
|
|
4454
|
+
dispatch({
|
|
4455
|
+
type: "layer/add",
|
|
4456
|
+
layer: {
|
|
4457
|
+
id,
|
|
4458
|
+
type: "text",
|
|
4459
|
+
transform: { x: 0.5, y: 0.4, scale: 1, rotation: 0 },
|
|
4460
|
+
opacity: 1,
|
|
4461
|
+
blendMode: "normal",
|
|
4462
|
+
data: {
|
|
4463
|
+
spans: [{ text: "Tap to edit", fontFamily: "System", fontSize: 0.07, color: "#ffffff" }],
|
|
4464
|
+
align: "center",
|
|
4465
|
+
maxWidth: 0.9
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
});
|
|
4469
|
+
select([id]);
|
|
4470
|
+
};
|
|
4471
|
+
const patchSpan = (patch) => {
|
|
4472
|
+
if (!selected || !span) return;
|
|
4473
|
+
dispatch({
|
|
4474
|
+
type: "layer/patchData",
|
|
4475
|
+
id: selected.id,
|
|
4476
|
+
patch: { spans: [{ ...span, ...patch }, ...data.spans.slice(1) ?? []] }
|
|
4477
|
+
});
|
|
4478
|
+
};
|
|
4479
|
+
const fonts = ["System", ...store.registry.fonts.map((f) => f.family)];
|
|
4480
|
+
if (!selected || !span || !data) {
|
|
4481
|
+
return /* @__PURE__ */ jsx32(View22, { style: { backgroundColor: theme.colors.background, padding: theme.spacing.md, alignItems: "center" }, children: /* @__PURE__ */ jsx32(
|
|
4482
|
+
Pressable18,
|
|
4483
|
+
{
|
|
4484
|
+
onPress: addText,
|
|
4485
|
+
style: {
|
|
4486
|
+
paddingHorizontal: theme.spacing.lg,
|
|
4487
|
+
paddingVertical: theme.spacing.sm,
|
|
4488
|
+
borderRadius: theme.radii.md,
|
|
4489
|
+
backgroundColor: theme.colors.surface
|
|
4490
|
+
},
|
|
4491
|
+
children: /* @__PURE__ */ jsx32(Text21, { style: { color: theme.colors.accent, fontSize: theme.font.size, fontWeight: theme.font.weight }, children: "+ Add text" })
|
|
4492
|
+
}
|
|
4493
|
+
) });
|
|
4494
|
+
}
|
|
4495
|
+
const cycleAlign = () => {
|
|
4496
|
+
const next = ALIGNS2[(ALIGNS2.indexOf(data.align) + 1) % ALIGNS2.length];
|
|
4497
|
+
dispatch({ type: "layer/patchData", id: selected.id, patch: { align: next } });
|
|
4498
|
+
};
|
|
4499
|
+
const cycleFill = () => {
|
|
4500
|
+
const current = data.fill?.mode;
|
|
4501
|
+
const index = FILL_MODES2.indexOf(current);
|
|
4502
|
+
const next = FILL_MODES2[(index + 1) % FILL_MODES2.length];
|
|
4503
|
+
dispatch({
|
|
4504
|
+
type: "layer/patchData",
|
|
4505
|
+
id: selected.id,
|
|
4506
|
+
patch: {
|
|
4507
|
+
fill: next ? { color: span.color === "#000000" ? "#ffffff" : "#000000", radius: 0.012, padding: 0.012, mode: next } : void 0
|
|
4508
|
+
}
|
|
4509
|
+
});
|
|
4510
|
+
};
|
|
4511
|
+
return /* @__PURE__ */ jsxs22(View22, { style: { backgroundColor: theme.colors.background }, children: [
|
|
4512
|
+
/* @__PURE__ */ jsxs22(View22, { style: { paddingHorizontal: theme.spacing.md }, children: [
|
|
4513
|
+
/* @__PURE__ */ jsx32(
|
|
4514
|
+
TextInput2,
|
|
4515
|
+
{
|
|
4516
|
+
value: span.text,
|
|
4517
|
+
onChangeText: (text) => patchSpan({ text }),
|
|
4518
|
+
multiline: true,
|
|
4519
|
+
placeholder: "Your text",
|
|
4520
|
+
placeholderTextColor: theme.colors.textMuted,
|
|
4521
|
+
style: {
|
|
4522
|
+
color: theme.colors.text,
|
|
4523
|
+
backgroundColor: theme.colors.surface,
|
|
4524
|
+
borderRadius: theme.radii.sm,
|
|
4525
|
+
padding: theme.spacing.sm,
|
|
4526
|
+
fontSize: theme.font.size
|
|
4527
|
+
}
|
|
4528
|
+
}
|
|
4529
|
+
),
|
|
4530
|
+
/* @__PURE__ */ jsx32(
|
|
4531
|
+
Slider,
|
|
4532
|
+
{
|
|
4533
|
+
value: span.fontSize,
|
|
4534
|
+
min: 0.02,
|
|
4535
|
+
max: 0.2,
|
|
4536
|
+
centered: false,
|
|
4537
|
+
onStart: () => dispatch({ type: "history/beginBatch", label: "font size" }),
|
|
4538
|
+
onChange: (fontSize) => patchSpan({ fontSize }),
|
|
4539
|
+
onEnd: () => dispatch({ type: "history/endBatch" })
|
|
4540
|
+
}
|
|
4541
|
+
)
|
|
4542
|
+
] }),
|
|
4543
|
+
/* @__PURE__ */ jsxs22(
|
|
4544
|
+
ScrollView11,
|
|
4545
|
+
{
|
|
4546
|
+
horizontal: true,
|
|
4547
|
+
showsHorizontalScrollIndicator: false,
|
|
4548
|
+
contentContainerStyle: { gap: theme.spacing.sm, paddingHorizontal: theme.spacing.md, alignItems: "center", paddingBottom: theme.spacing.sm },
|
|
4549
|
+
style: { flexGrow: 0 },
|
|
4550
|
+
children: [
|
|
4551
|
+
/* @__PURE__ */ jsx32(Pressable18, { onPress: cycleAlign, style: { padding: theme.spacing.sm }, children: /* @__PURE__ */ jsx32(Text21, { style: { color: theme.colors.text, fontWeight: theme.font.weight }, children: data.align === "left" ? "\u21E4" : data.align === "center" ? "\u2630" : "\u21E5" }) }),
|
|
4552
|
+
/* @__PURE__ */ jsx32(Pressable18, { onPress: cycleFill, style: { padding: theme.spacing.sm }, children: /* @__PURE__ */ jsx32(Text21, { style: { color: data.fill ? theme.colors.accent : theme.colors.text, fontWeight: theme.font.weight }, children: data.fill?.mode === "perLine" ? "A\u20DE\u20DE" : "A\u20DE" }) }),
|
|
4553
|
+
fonts.map((family) => /* @__PURE__ */ jsx32(Pressable18, { onPress: () => patchSpan({ fontFamily: family }), style: { padding: theme.spacing.sm }, children: /* @__PURE__ */ jsx32(
|
|
4554
|
+
Text21,
|
|
4555
|
+
{
|
|
4556
|
+
style: {
|
|
4557
|
+
color: span.fontFamily === family ? theme.colors.accent : theme.colors.textMuted,
|
|
4558
|
+
fontSize: theme.font.sizeSm
|
|
4559
|
+
},
|
|
4560
|
+
children: family
|
|
4561
|
+
}
|
|
4562
|
+
) }, family)),
|
|
4563
|
+
SWATCHES3.map((color) => /* @__PURE__ */ jsx32(
|
|
4564
|
+
Pressable18,
|
|
4565
|
+
{
|
|
4566
|
+
onPress: () => patchSpan({ color }),
|
|
4567
|
+
style: {
|
|
4568
|
+
width: 24,
|
|
4569
|
+
height: 24,
|
|
4570
|
+
borderRadius: 12,
|
|
4571
|
+
backgroundColor: color,
|
|
4572
|
+
borderWidth: span.color === color ? 3 : 1,
|
|
4573
|
+
borderColor: span.color === color ? theme.colors.accent : theme.colors.track
|
|
4574
|
+
}
|
|
4575
|
+
},
|
|
4576
|
+
color
|
|
4577
|
+
))
|
|
4578
|
+
]
|
|
4579
|
+
}
|
|
4580
|
+
)
|
|
4581
|
+
] });
|
|
4582
|
+
}
|
|
4583
|
+
export {
|
|
4584
|
+
AddPhotoPanel,
|
|
4585
|
+
AdjustPanel,
|
|
4586
|
+
CanvasStage,
|
|
4587
|
+
CropGestureSurface,
|
|
4588
|
+
CropGrid,
|
|
4589
|
+
CropPanel,
|
|
4590
|
+
DEFAULT_BRUSH,
|
|
4591
|
+
DrawPanel,
|
|
4592
|
+
DrawingSurface,
|
|
4593
|
+
Editor,
|
|
4594
|
+
EditorGestures,
|
|
4595
|
+
FilterStrip,
|
|
4596
|
+
HostCallbacksProvider,
|
|
4597
|
+
InteractionGuides,
|
|
4598
|
+
InteractionProvider,
|
|
4599
|
+
InvalidImageResultError,
|
|
4600
|
+
LayerGestureSurface,
|
|
4601
|
+
LayersPanel,
|
|
4602
|
+
LayoutPanel,
|
|
4603
|
+
MediaPanel,
|
|
4604
|
+
POST_PROFILE,
|
|
4605
|
+
STORY_PROFILE,
|
|
4606
|
+
STRAIGHTEN_LIMIT,
|
|
4607
|
+
Slider,
|
|
4608
|
+
StickerPanel,
|
|
4609
|
+
StoryMediaGestureSurface,
|
|
4610
|
+
StraightenRuler,
|
|
4611
|
+
TextComposer,
|
|
4612
|
+
TextComposerHost,
|
|
4613
|
+
TextPanel,
|
|
4614
|
+
ThemeContext,
|
|
4615
|
+
ToolPanelHost,
|
|
4616
|
+
ToolRail,
|
|
4617
|
+
TopBar,
|
|
4618
|
+
UiStateProvider,
|
|
4619
|
+
canvasSizeForAspect,
|
|
4620
|
+
cellToTransformPatch,
|
|
4621
|
+
centeredCropForAspect,
|
|
4622
|
+
createAddPhotoController,
|
|
4623
|
+
createDrawController,
|
|
4624
|
+
createGestureController,
|
|
4625
|
+
createUiStateStore,
|
|
4626
|
+
defaultTheme,
|
|
4627
|
+
layoutCells,
|
|
4628
|
+
mergeSlots,
|
|
4629
|
+
mergeTheme,
|
|
4630
|
+
radiansToStraighten,
|
|
4631
|
+
resolveGestureSurface,
|
|
4632
|
+
resolvePanel,
|
|
4633
|
+
straightenToRadians,
|
|
4634
|
+
useBaseImageLayer,
|
|
4635
|
+
useBaseMediaLayer,
|
|
4636
|
+
useHostCallbacks,
|
|
4637
|
+
useInteractionState,
|
|
4638
|
+
useTargetImageLayer,
|
|
4639
|
+
useTextSession,
|
|
4640
|
+
useTheme,
|
|
4641
|
+
useUiState
|
|
4642
|
+
};
|
|
4643
|
+
//# sourceMappingURL=index.mjs.map
|