diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fcc3578 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,134 @@ +{ + "name": "ebox", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@rollup/plugin-typescript": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-5.0.2.tgz", + "integrity": "sha512-CkS028Itwjqm1uLbFVfpJgtVtnNvZ+og/m6UlNRR5wOOnNTWPcVQzOu5xGdEX+WWJxdvWIqUq2uR/RBt2ZipWg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.0.1", + "resolve": "^1.14.1" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + } + } + }, + "estree-walker": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.1.tgz", + "integrity": "sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg==", + "dev": true + }, + "expression-globals-typescript": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/expression-globals-typescript/-/expression-globals-typescript-1.1.1.tgz", + "integrity": "sha512-FGKgD+85oNDQs/W7INJBNm1A/Ve0jOylm2SVZHtxj0e+a/+nMLfOagWZNjTUpgp4jNtjjTFzoLaBcGvmdeNtRQ==" + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "rollup": { + "version": "2.26.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.26.3.tgz", + "integrity": "sha512-Mlt39/kL2rA9egcbQbaZV1SNVplGqYYhDDMcGgHPPE0tvM3R4GrB+IEdYy2QtTrdzMQx57ZcqDFf/KWWm8F+uw==", + "dev": true, + "requires": { + "fsevents": "~2.1.2" + } + }, + "rollup-plugin-ae-jsx": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-ae-jsx/-/rollup-plugin-ae-jsx-1.1.3.tgz", + "integrity": "sha512-CfKql2qRyShCImyOykjULAbZGGdr9Zb8juClhTh1LAY5EuFkpvDspT+63a2wCw8LB5nTdCxM+/D3EMtXOGwEMg==", + "dev": true, + "requires": { + "estree-walker": "^2.0.1", + "magic-string": "^0.25.7" + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", + "dev": true + }, + "typescript": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..586b8a8 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "ebox", + "version": "1.0.0", + "description": "Create rectangles within After Effects expressions ", + "main": "src/index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "tsc": "tsc", + "build": "rollup -c", + "watch": "rollup -cw", + "release": "hub release create -a 'dist/eKeys.jsx" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/motiondeveloper/ebox.git" + }, + "author": "Tim Haywood", + "license": "MIT", + "bugs": { + "url": "https://github.com/motiondeveloper/ebox/issues" + }, + "homepage": "https://github.com/motiondeveloper/ebox#readme", + "devDependencies": { + "@rollup/plugin-typescript": "^5.0.2", + "prettier": "^1.16.4", + "rollup": "^2.26.3", + "rollup-plugin-ae-jsx": "^1.1.3", + "tslib": "^2.0.1", + "typescript": "^3.9.7" + }, + "dependencies": { + "expression-globals-typescript": "^1.1.1" + } +} diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..c8b477c --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,4 @@ +module.exports = { + trailingComma: "es5", + singleQuote: true, +}; diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..c2fbac5 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,21 @@ +import typescript from '@rollup/plugin-typescript'; +import afterEffectJsx from 'rollup-plugin-ae-jsx'; + +export default { + input: 'src/index.ts', + output: { + file: 'dist/eBox.jsx', + format: 'cjs', + }, + plugins: [ + typescript({ + module: 'esnext', + target: 'esnext', + noImplicitAny: true, + moduleResolution: 'node', + strict: true, + lib: ['esnext'], + }), + afterEffectJsx(), + ], +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..005c66a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,176 @@ +import { + LayerBase, + PropertyBase, + Vector2D, + Points, +} from 'expression-globals-typescript'; + +// Creating layer and property mocks +const thisLayer = Object.create(LayerBase); +const thisProperty = Object.create(PropertyBase); + +// eBox types +type Anchor = 'topLeft' | 'topRight' | 'bottomRight' | 'bottomLeft' | 'center'; +interface BoxProps { + size: Vector2D; + position: Vector2D; + anchor: Anchor; + isClosed: boolean; +} + +function createBox({ + size = [100, 100], + position = [0, 0], + anchor = 'center', + isClosed = true, +}: BoxProps) { + const pointOrder: Anchor[] = [ + 'topLeft', + 'topRight', + 'bottomRight', + 'bottomLeft', + ]; + + function positionToCenter( + position: Vector2D, + size: Vector2D, + anchor: Anchor + ): Vector2D { + const positionCalculations = { + center: (): Vector2D => position, + topLeft: (): Vector2D => [ + position[0] + size[0] / 2, + position[1] + size[1] / 2, + ], + topRight: (): Vector2D => [ + position[0] - size[0] / 2, + position[1] + size[1] / 2, + ], + bottomLeft: (): Vector2D => [ + position[0] + size[0] / 2, + position[1] - size[1] / 2, + ], + bottomRight: (): Vector2D => [ + position[0] - size[0] / 2, + position[1] - size[1] / 2, + ], + }; + + return positionCalculations[anchor](); + } + + function sizeToPoints(size: Vector2D): Points { + return [ + [-size[0] / 2, -size[1] / 2], + [size[0] / 2, -size[1] / 2], + [size[0] / 2, size[1] / 2], + [-size[0] / 2, size[1] / 2], + ]; + } + function movePoints( + points: Points, + oldPosition: Vector2D, + newPosition: Vector2D + ): Points { + const positionDelta: Vector2D = newPosition.map( + (dimension, dimensionIndex): number => { + return dimension - oldPosition[dimensionIndex]; + } + ) as Vector2D; + + return points.map( + (point: Vector2D): Vector2D => { + return point.map((dimension, dimensionIndex) => { + return dimension + positionDelta[dimensionIndex]; + }) as Vector2D; + } + ) as Points; + } + + function pointsToComp(points: Points): Points { + return points.map( + (point): Vector2D => thisLayer.fromCompToSurface(point) as Vector2D + ) as Points; + } + function pointsToPath(points: Points, isClosed: boolean) { + return thisProperty.createPath(points, [], [], isClosed); + } + + const centerPosition = positionToCenter(position, size, anchor); + interface OutputBox extends BoxProps { + centerPosition: Vector2D; + } + let boxPoints: Points = createPointsFromBoxProps({ + size, + position, + anchor, + isClosed, + centerPosition, + }); + + function getBoxPath() { + return pointsToPath(boxPoints, isClosed); + } + function createPointsFromBoxProps(boxProps: OutputBox): Points { + const points = sizeToPoints(boxProps.size); + const centeredPoints = movePoints(points, [0, 0], boxProps.centerPosition); + const compPositionPoints = pointsToComp(centeredPoints); + + return compPositionPoints; + } + + function scalePoints(scale: Vector2D = [100, 100], anchor: Anchor): void { + // Remap scale to [0..1] + const normalizedScale: Vector2D = scale.map( + scale => scale / 100 + ) as Vector2D; + + // Get index of anchor point + const anchorPointIndex: number = pointOrder.indexOf(anchor); + const anchorPoint: Vector2D = boxPoints[anchorPointIndex]; + + // Calculate distance from anchor point + const pointDeltas: Points = boxPoints.map(point => { + return point.map((dimension, dimensionIndex): number => { + return dimension - anchorPoint[dimensionIndex]; + }) as Vector2D; + }) as Points; + + // Scale the point deltas according to input scale + const scaledPointDeltas: Points = pointDeltas.map( + (point): Vector2D => { + return point.map((dimension, dimensionIndex): number => { + return dimension * normalizedScale[dimensionIndex]; + }) as Vector2D; + } + ) as Points; + + const scaledPoints: Points = boxPoints.map( + (point, pointIndex): Vector2D => { + if (pointIndex !== anchorPointIndex) { + // If not the anchor point + // Create the point from the scaledPointDelta + return point.map((pointDimension, dimensionIndex): number => { + return ( + anchorPoint[dimensionIndex] + + scaledPointDeltas[pointIndex][dimensionIndex] + ); + }) as Vector2D; + } else { + // If the anchor point + // Return as is + return point; + } + } + ) as Points; + + boxPoints = scaledPoints; + } + + return { + setScale: scalePoints, + getPath: getBoxPath, + }; +} + +export { createBox };