|
7 | 7 | * pretty-printer churn. Consumes ParsedGsapAcornForWrite from gsapParserAcorn.ts. |
8 | 8 | */ |
9 | 9 | import MagicString from "magic-string"; |
10 | | -import type { GsapAnimation, GsapPercentageKeyframe } from "./gsapSerialize.js"; |
11 | | -import { resolveConversionProps } from "./gsapSerialize.js"; |
| 10 | +import type { |
| 11 | + GsapAnimation, |
| 12 | + GsapPercentageKeyframe, |
| 13 | + ArcPathConfig, |
| 14 | + ArcPathSegment, |
| 15 | +} from "./gsapSerialize.js"; |
| 16 | +import { |
| 17 | + resolveConversionProps, |
| 18 | + extractArcWaypoints, |
| 19 | + buildMotionPathObjectCode, |
| 20 | +} from "./gsapSerialize.js"; |
12 | 21 | import { |
13 | 22 | parseGsapScriptAcornForWrite, |
14 | 23 | type ParsedGsapAcornForWrite, |
@@ -1174,6 +1183,160 @@ export function removeLabelFromScript(script: string, name: string): string { |
1174 | 1183 | return ms.toString(); |
1175 | 1184 | } |
1176 | 1185 |
|
| 1186 | +// ── Arc path helpers ───────────────────────────────────────────────────────── |
| 1187 | + |
| 1188 | +/** |
| 1189 | + * Remove a set of properties from an ObjectExpression in a single pass. |
| 1190 | + * Groups consecutive marked props into blocks to avoid overlapping remove ranges. |
| 1191 | + */ |
| 1192 | +function removePropsByKey(ms: MagicString, objNode: any, keys: Set<string>): void { |
| 1193 | + if (objNode?.type !== "ObjectExpression") return; |
| 1194 | + const allProps = (objNode.properties ?? []).filter(isObjectProperty); |
| 1195 | + const marked = allProps.map((p: any) => keys.has(propKeyName(p) ?? "")); |
| 1196 | + let i = 0; |
| 1197 | + while (i < allProps.length) { |
| 1198 | + if (!marked[i]) { |
| 1199 | + i++; |
| 1200 | + continue; |
| 1201 | + } |
| 1202 | + const blockStart = i; |
| 1203 | + while (i < allProps.length && marked[i]) i++; |
| 1204 | + ms.remove(...blockRemoveRange(allProps, blockStart, i)); |
| 1205 | + } |
| 1206 | +} |
| 1207 | + |
| 1208 | +function blockRemoveRange(allProps: any[], blockStart: number, blockEnd: number): [number, number] { |
| 1209 | + if (blockStart === 0 && blockEnd === allProps.length) |
| 1210 | + return [allProps[0].start, allProps[allProps.length - 1].end]; |
| 1211 | + if (blockStart === 0) return [allProps[0].start, allProps[blockEnd].start]; |
| 1212 | + return [allProps[blockStart - 1].end, allProps[blockEnd - 1].end]; |
| 1213 | +} |
| 1214 | + |
| 1215 | +// fallow-ignore-next-line complexity |
| 1216 | +function readLastWaypointXY(mpVal: any): { x: number | null; y: number | null } { |
| 1217 | + if (mpVal?.type !== "ObjectExpression") return { x: null, y: null }; |
| 1218 | + const pathProp = findPropertyNode(mpVal, "path"); |
| 1219 | + if (pathProp?.value?.type !== "ArrayExpression") return { x: null, y: null }; |
| 1220 | + const elems: any[] = pathProp.value.elements ?? []; |
| 1221 | + const last = elems[elems.length - 1]; |
| 1222 | + if (last?.type !== "ObjectExpression") return { x: null, y: null }; |
| 1223 | + const xRaw = findPropertyNode(last, "x")?.value?.value; |
| 1224 | + const yRaw = findPropertyNode(last, "y")?.value?.value; |
| 1225 | + return { x: typeof xRaw === "number" ? xRaw : null, y: typeof yRaw === "number" ? yRaw : null }; |
| 1226 | +} |
| 1227 | + |
| 1228 | +function disableArcPath(ms: MagicString, call: TweenCallInfo): boolean { |
| 1229 | + const mpProp = findPropertyNode(call.varsArg, "motionPath"); |
| 1230 | + if (!mpProp) return false; |
| 1231 | + const { x, y } = readLastWaypointXY(mpProp.value); |
| 1232 | + if (x === null && y === null) { |
| 1233 | + const allProps = (call.varsArg.properties ?? []).filter(isObjectProperty); |
| 1234 | + removeProp(ms, mpProp, allProps); |
| 1235 | + return true; |
| 1236 | + } |
| 1237 | + // Overwrite the entire motionPath property with the recovered x/y pair — avoids |
| 1238 | + // the appendLeft+remove range-boundary issue in MagicString. |
| 1239 | + const parts: string[] = []; |
| 1240 | + if (x !== null) parts.push(`x: ${x}`); |
| 1241 | + if (y !== null) parts.push(`y: ${y}`); |
| 1242 | + ms.overwrite(mpProp.start, mpProp.end, parts.join(", ")); |
| 1243 | + return true; |
| 1244 | +} |
| 1245 | + |
| 1246 | +function stripXYFromKeyframes(ms: MagicString, kfPropNode: any): void { |
| 1247 | + if (kfPropNode?.value?.type !== "ObjectExpression") return; |
| 1248 | + const xyKeys = new Set(["x", "y"]); |
| 1249 | + for (const pctProp of (kfPropNode.value.properties ?? []).filter(isObjectProperty)) { |
| 1250 | + const k = propKeyName(pctProp); |
| 1251 | + if (typeof k === "string" && k.endsWith("%") && pctProp.value?.type === "ObjectExpression") { |
| 1252 | + removePropsByKey(ms, pctProp.value, xyKeys); |
| 1253 | + } |
| 1254 | + } |
| 1255 | +} |
| 1256 | + |
| 1257 | +function enableArcPath( |
| 1258 | + ms: MagicString, |
| 1259 | + call: TweenCallInfo, |
| 1260 | + animation: GsapAnimation, |
| 1261 | + config: ArcPathConfig, |
| 1262 | +): boolean { |
| 1263 | + const waypoints = extractArcWaypoints(animation); |
| 1264 | + if (waypoints.length < 2) return false; |
| 1265 | + const segments: ArcPathSegment[] = |
| 1266 | + config.segments.length === waypoints.length - 1 |
| 1267 | + ? config.segments |
| 1268 | + : Array.from({ length: waypoints.length - 1 }, () => ({ curviness: 1 })); |
| 1269 | + const motionPathCode = buildMotionPathObjectCode({ |
| 1270 | + waypoints, |
| 1271 | + segments, |
| 1272 | + autoRotate: config.autoRotate, |
| 1273 | + }); |
| 1274 | + upsertProp(ms, call.varsArg, "motionPath", `__raw:${motionPathCode}`); |
| 1275 | + stripXYFromKeyframes(ms, findPropertyNode(call.varsArg, "keyframes")); |
| 1276 | + removePropsByKey(ms, call.varsArg, new Set(["x", "y"])); |
| 1277 | + return true; |
| 1278 | +} |
| 1279 | + |
| 1280 | +export function setArcPathInScript( |
| 1281 | + script: string, |
| 1282 | + animationId: string, |
| 1283 | + config: ArcPathConfig, |
| 1284 | +): string { |
| 1285 | + const parsed = parseGsapScriptAcornForWrite(script); |
| 1286 | + if (!parsed) return script; |
| 1287 | + const target = parsed.located.find((l) => l.id === animationId); |
| 1288 | + if (!target) return script; |
| 1289 | + const ms = new MagicString(script); |
| 1290 | + const handled = config.enabled |
| 1291 | + ? enableArcPath(ms, target.call, target.animation, config) |
| 1292 | + : disableArcPath(ms, target.call); |
| 1293 | + return handled ? ms.toString() : script; |
| 1294 | +} |
| 1295 | + |
| 1296 | +export function updateArcSegmentInScript( |
| 1297 | + script: string, |
| 1298 | + animationId: string, |
| 1299 | + segmentIndex: number, |
| 1300 | + update: Partial<ArcPathSegment>, |
| 1301 | +): string { |
| 1302 | + const parsed = parseGsapScriptAcornForWrite(script); |
| 1303 | + if (!parsed) return script; |
| 1304 | + const target = parsed.located.find((l) => l.id === animationId); |
| 1305 | + if (!target) return script; |
| 1306 | + |
| 1307 | + const { call, animation } = target; |
| 1308 | + if (!animation.arcPath?.enabled) return script; |
| 1309 | + |
| 1310 | + const segments = [...animation.arcPath.segments]; |
| 1311 | + if (segmentIndex < 0 || segmentIndex >= segments.length) return script; |
| 1312 | + |
| 1313 | + segments[segmentIndex] = { ...segments[segmentIndex]!, ...update }; |
| 1314 | + |
| 1315 | + const waypoints = extractArcWaypoints(animation); |
| 1316 | + if (waypoints.length < 2) return script; |
| 1317 | + |
| 1318 | + const motionPathCode = buildMotionPathObjectCode({ |
| 1319 | + waypoints, |
| 1320 | + segments, |
| 1321 | + autoRotate: animation.arcPath.autoRotate, |
| 1322 | + }); |
| 1323 | + |
| 1324 | + const mpProp = findPropertyNode(call.varsArg, "motionPath"); |
| 1325 | + if (!mpProp) return script; |
| 1326 | + |
| 1327 | + const ms = new MagicString(script); |
| 1328 | + ms.overwrite(mpProp.value.start, mpProp.value.end, motionPathCode); |
| 1329 | + return ms.toString(); |
| 1330 | +} |
| 1331 | + |
| 1332 | +export function removeArcPathFromScript(script: string, animationId: string): string { |
| 1333 | + return setArcPathInScript(script, animationId, { |
| 1334 | + enabled: false, |
| 1335 | + autoRotate: false, |
| 1336 | + segments: [], |
| 1337 | + }); |
| 1338 | +} |
| 1339 | + |
1177 | 1340 | // ── splitAnimationsInScript helpers ────────────────────────────────────────── |
1178 | 1341 |
|
1179 | 1342 | /** Overwrite the selector (first arg) of a tween call. */ |
|
0 commit comments