├── .gitignore
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── README.md
├── esbuild.config.js
├── examples
├── data.json
└── index.html
├── package.json
├── src
├── Animation.ts
├── core.ts
├── effects
│ ├── Blur.ts
│ └── DropShadow.ts
├── index.ts
├── layers
│ ├── BaseLayer.ts
│ ├── CompLayer.ts
│ ├── ImageLayer.ts
│ ├── NullLayer.ts
│ ├── TextLayer.ts
│ └── VectorLayer.ts
├── objects
│ ├── AnimatedPath.ts
│ ├── Ellipse.ts
│ ├── Group.ts
│ ├── Path.ts
│ ├── Polystar.ts
│ └── Rect.ts
├── property
│ ├── AnimatedProperty.ts
│ ├── BlendingMode.ts
│ ├── Fill.ts
│ ├── GradientFill.ts
│ ├── Property.ts
│ ├── Stroke.ts
│ └── Trim.ts
├── transform
│ ├── Position.ts
│ └── Transform.ts
└── utils
│ ├── Bezier.ts
│ └── BezierEasing.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS generated files #
2 | ######################
3 | .DS_Store
4 | .DS_Store?
5 | .AppleDouble
6 | .LSOverride
7 | .Spotlight-V100
8 | .Trashes
9 | ._*
10 | ehthumbs.db
11 | Thumbs.db
12 |
13 | # Folder config file
14 | Desktop.ini
15 |
16 | # Recycle Bin used on file shares
17 | $RECYCLE.BIN/
18 |
19 | # Webstorm Files #
20 | ##################
21 | ## Directory-based project format
22 | .idea/
23 | # if you remove the above rule, at least ignore user-specific stuff:
24 | # .idea/workspace.xml
25 | # .idea/tasks.xml
26 | # and these sensitive or high-churn files:
27 | # .idea/dataSources.ids
28 | # .idea/dataSources.xml
29 | # .idea/sqlDataSources.xml
30 | # .idea/dynamic.xml
31 |
32 |
33 | # Packages #
34 | ############
35 | # it's better to unpack these files and commit the raw source
36 | # git has its own built in compression methods
37 | *.7z
38 | *.dmg
39 | *.gz
40 | *.iso
41 | *.jar
42 | *.rar
43 | *.tar
44 | *.zip
45 |
46 | # Node Package Manager Files
47 | npm-debug.log
48 | node_modules/
49 |
50 | # Bower Files
51 | bower_components/
52 |
53 | dist/
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 19.8.1
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /dist
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 4,
4 | "semi": false,
5 | "singleQuote": true,
6 | "useTabs": true,
7 | "printWidth": 120
8 | }
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AE2Canvas
2 |
--------------------------------------------------------------------------------
/esbuild.config.js:
--------------------------------------------------------------------------------
1 | import { context } from 'esbuild'
2 | import fs from 'fs-extra'
3 |
4 | const dist_folder = 'dist'
5 | const dev = process.argv[2] === 'mode=development'
6 |
7 | fs.removeSync(dist_folder)
8 | fs.mkdirpSync(dist_folder)
9 |
10 | const ctx = await context({
11 | entryPoints: ['./src/index.js'],
12 | outdir: dist_folder,
13 | format: 'esm',
14 | logLevel: 'debug',
15 | bundle: true,
16 | minify: true,
17 | sourcemap: true,
18 | })
19 |
20 | if (dev) {
21 | await ctx.watch()
22 | await ctx.serve({ servedir: '/' })
23 | } else {
24 | await ctx.rebuild()
25 | await ctx.cancel()
26 | await ctx.dispose()
27 | }
28 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@kilokilo/ae2canvas",
3 | "version": "1.7.3",
4 | "main": "dist/index.js",
5 | "module": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "author": "Lukas Zgraggen ",
8 | "type": "module",
9 | "devDependencies": {
10 | "esbuild": "0.17.19",
11 | "fs-extra": "11.1.1",
12 | "path": "0.12.7",
13 | "prettier": "2.8.8",
14 | "typescript": "5.0.4"
15 | },
16 | "dependencies": {
17 | "tiny-emitter": "2.0.2"
18 | },
19 | "scripts": {
20 | "dev": "node esbuild.config.js mode=development",
21 | "build": "node esbuild.config.js mode=production && tsc",
22 | "prepublishOnly": "yarn run build"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Animation.ts:
--------------------------------------------------------------------------------
1 | import { add, remove } from './core'
2 | import Emitter from 'tiny-emitter'
3 | import ImageLayer from './layers/ImageLayer'
4 | import NullLayer from './layers/NullLayer'
5 | import TextLayer from './layers/TextLayer'
6 | import CompLayer from './layers/CompLayer'
7 | import VectorLayer from './layers/VectorLayer'
8 | import GradientFill from './property/GradientFill'
9 | import BaseLayer, { BaseLayerProps } from './layers/BaseLayer'
10 |
11 | type Marker = {
12 | time: number
13 | stop: number
14 | comment: string
15 | }
16 |
17 | type AnimationProps = {
18 | reversed?: boolean
19 | canvas?: HTMLCanvasElement
20 | imageBasePath?: string
21 | fluid?: boolean
22 | devicePixelRatio?: number
23 | loop?: boolean
24 | baseFont?: string
25 |
26 | data: {
27 | layers: []
28 | comps: { [key: string]: BaseLayerProps }
29 | markers: Marker[]
30 | height: number
31 | width: number
32 | duration: number
33 | }
34 | }
35 |
36 | export type Gradients = { [key: string]: GradientFill[] }
37 |
38 | class Animation extends Emitter {
39 | private readonly gradients: Gradients
40 | private readonly duration: number
41 | private readonly baseWidth: number
42 | private readonly baseHeight: number
43 | private readonly ratio: number
44 | private readonly markers: Marker[]
45 | private readonly baseFont?: string
46 | private readonly loop: boolean
47 | private readonly devicePixelRatio: number
48 | private readonly fluid: boolean
49 | private readonly imageBasePath: string
50 | private readonly canvas: HTMLCanvasElement
51 | private readonly ctx: CanvasRenderingContext2D | null
52 | private readonly layers: BaseLayer[]
53 |
54 | private _reversed = false
55 | private isPaused = false
56 | private isPlaying = false
57 | private drawFrame = true
58 |
59 | private pausedTime = 0
60 | private time = 0
61 | private scale = 1
62 | private then = 0
63 |
64 | constructor(options: AnimationProps) {
65 | super()
66 |
67 | this.gradients = {}
68 | this.duration = options.data.duration
69 | this.baseWidth = options.data.width
70 | this.baseHeight = options.data.height
71 | this.ratio = options.data.width / options.data.height
72 | this.markers = options.data.markers
73 | this.baseFont = options.baseFont
74 | this.loop = options.loop || false
75 | this.devicePixelRatio =
76 | options.devicePixelRatio ||
77 | (typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1)
78 | this.fluid = options.fluid || true
79 | this.imageBasePath = options.imageBasePath || ''
80 | const comps = options.data.comps
81 |
82 | this.canvas = options.canvas || document.createElement('canvas')
83 | this.canvas.width = this.baseWidth
84 | this.canvas.height = this.baseHeight
85 | this.ctx = this.canvas.getContext('2d')
86 |
87 | this.layers = options.data.layers.map((layer: BaseLayerProps) => {
88 | switch (layer.type) {
89 | case 'vector':
90 | return new VectorLayer(layer, this.gradients)
91 | case 'image':
92 | return new ImageLayer(layer, this.imageBasePath)
93 | case 'text':
94 | return new TextLayer(layer, this.baseFont)
95 | case 'comp':
96 | return new CompLayer(layer, comps, this.baseFont, this.gradients, this.imageBasePath)
97 | case 'null':
98 | default:
99 | return new NullLayer(layer)
100 | }
101 | })
102 |
103 | this.layers.forEach((layer) => {
104 | layer.parent = this.layers.find((l) => l.index === layer.parentIndex)
105 | })
106 |
107 | this.reversed = options.reversed || false
108 | this.reset()
109 | this.resize()
110 |
111 | add(this)
112 | }
113 |
114 | play() {
115 | if (!this.isPlaying) {
116 | if (!this.isPaused) this.reset()
117 | this.isPaused = false
118 | this.pausedTime = 0
119 | this.isPlaying = true
120 | }
121 | }
122 |
123 | stop() {
124 | this.reset()
125 | this.isPlaying = false
126 | this.drawFrame = true
127 | }
128 |
129 | pause() {
130 | if (this.isPlaying) {
131 | this.isPaused = true
132 | this.pausedTime = this.time
133 | this.isPlaying = false
134 | }
135 | }
136 |
137 | gotoAndPlay(id: number | string) {
138 | const marker = this.getMarker(id)
139 | if (marker) {
140 | this.time = marker.time
141 | this.pausedTime = 0
142 | this.setKeyframes(this.time)
143 | this.isPlaying = true
144 | }
145 | }
146 |
147 | gotoAndStop(id: number | string) {
148 | const marker = this.getMarker(id)
149 | if (marker) {
150 | this.time = marker.time
151 | this.setKeyframes(this.time)
152 | this.drawFrame = true
153 | this.isPlaying = false
154 | }
155 | }
156 |
157 | getMarker(id: number | string) {
158 | let marker
159 | if (typeof id === 'number') {
160 | marker = this.markers[id]
161 | } else {
162 | marker = this.markers.find((marker) => marker.comment === id)
163 | }
164 |
165 | if (marker) return marker
166 | console.warn('Marker not found')
167 | }
168 |
169 | getLayerByName(name: string): BaseLayer | null {
170 | const getLayer = (name: string, layer: BaseLayer): BaseLayer | null => {
171 | if (name === layer.name) {
172 | return layer
173 | } else if (layer.layers) {
174 | for (let i = 0; i < layer.layers.length; i += 1) {
175 | const hit = getLayer(name, layer.layers[i])
176 | if (hit) {
177 | return hit
178 | }
179 | }
180 | }
181 | return null
182 | }
183 |
184 | return this.layers.find((layer) => getLayer(name, layer)) || null
185 | }
186 |
187 | checkStopMarkers(from: number, to: number) {
188 | return this.markers.find((marker) => marker.stop && marker.time > from && marker.time < to)
189 | }
190 |
191 | preload() {
192 | const promises: Promise[] = []
193 | const preloadLayer = (layers: BaseLayer[], promises: Promise[]) => {
194 | layers.forEach((layer) => {
195 | if (layer instanceof ImageLayer) {
196 | promises.push(layer.preload())
197 | } else if (layer instanceof CompLayer) {
198 | preloadLayer(layer.layers, promises)
199 | }
200 | })
201 | }
202 |
203 | preloadLayer(this.layers, promises)
204 | return Promise.all(promises).catch((error) => console.error(error))
205 | }
206 |
207 | reset() {
208 | this.pausedTime = 0
209 | this.time = this.reversed ? this.duration : 0
210 | this.layers.forEach((layer) => layer.reset(this.reversed))
211 | }
212 |
213 | setKeyframes(time: number) {
214 | this.layers.forEach((layer) => layer.setKeyframes(time))
215 | }
216 |
217 | destroy() {
218 | this.isPlaying = false
219 | if (this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas)
220 | remove(this)
221 | }
222 |
223 | resize(w?: number) {
224 | if (this.fluid) {
225 | const width = w || this.canvas.clientWidth || this.baseWidth
226 | this.canvas.width = width * this.devicePixelRatio
227 | this.canvas.height = (width / this.ratio) * this.devicePixelRatio
228 |
229 | this.scale = (width / this.baseWidth) * this.devicePixelRatio
230 | this.ctx?.transform(this.scale, 0, 0, this.scale, 0, 0)
231 | this.setKeyframes(this.time)
232 | this.drawFrame = true
233 | }
234 | }
235 |
236 | setGradients(name: string, stops: any) {
237 | if (!this.gradients[name]) {
238 | console.warn(`Gradient with name: ${name} not found.`)
239 | return
240 | }
241 |
242 | this.gradients[name].forEach((gradient) => {
243 | gradient.stops = stops
244 | })
245 | }
246 |
247 | getSpriteSheet(fps = 25, width = 50, maxWidth = 4096) {
248 | const ratio = width / this.baseWidth
249 | const height = this.baseHeight * ratio
250 | const numFrames = Math.floor((this.duration / 1000) * fps)
251 | const buffer = document.createElement('canvas')
252 | const ctx = buffer.getContext('2d')
253 |
254 | const rowsX = Math.floor(maxWidth / width)
255 | const rowsY = Math.ceil(numFrames / rowsX)
256 |
257 | let indexX = 0
258 | let indexY = 0
259 |
260 | buffer.width = rowsX * width
261 | buffer.height = rowsY * height
262 |
263 | this.resize(width)
264 |
265 | for (let i = 0; i < numFrames; i++) {
266 | const step = i / numFrames
267 | const time = step * this.duration
268 | this.setKeyframes(time)
269 | this.draw(time)
270 |
271 | const x = indexX * width
272 | const y = indexY * height
273 |
274 | if (indexX + 1 >= rowsX) {
275 | indexX = 0
276 | indexY++
277 | } else {
278 | indexX++
279 | }
280 |
281 | ctx?.drawImage(this.canvas, x, y, width, height)
282 | }
283 |
284 | return {
285 | frames: numFrames,
286 | canvas: buffer,
287 | offsetX: width,
288 | offsetY: height,
289 | rowsX,
290 | rowsY,
291 | }
292 | }
293 |
294 | draw(time: number) {
295 | if (!this.ctx) return
296 |
297 | this.ctx.clearRect(0, 0, this.baseWidth, this.baseHeight)
298 |
299 | this.layers.forEach((layer) => {
300 | if (this.ctx && time >= layer.in && time <= layer.out) {
301 | layer.draw(this.ctx, time, this.scale)
302 | }
303 | })
304 | }
305 |
306 | update(time: number) {
307 | if (!this.then) this.then = time
308 |
309 | const delta = time - this.then
310 | this.then = time
311 |
312 | if (this.isPlaying) {
313 | this.time = this.reversed ? this.time - delta : this.time + delta
314 |
315 | const stopMarker = this.checkStopMarkers(this.time - delta, this.time)
316 |
317 | if (this.time > this.duration || (this.reversed && this.time < 0)) {
318 | this.time = this.reversed ? 0 : this.duration - 1
319 | this.isPlaying = false
320 | this.emit('complete')
321 | if (this.loop) {
322 | this.play()
323 | }
324 | } else if (stopMarker) {
325 | this.time = stopMarker.time
326 | this.pause()
327 | this.emit('stop', stopMarker)
328 | } else {
329 | this.draw(this.time)
330 | }
331 | this.emit('update')
332 | } else if (this.drawFrame) {
333 | this.drawFrame = false
334 | this.draw(this.time)
335 | this.emit('update')
336 | }
337 | }
338 |
339 | set step(step) {
340 | this.isPlaying = false
341 | this.time = step * this.duration
342 | this.pausedTime = this.time
343 | this.setKeyframes(this.time)
344 | this.drawFrame = true
345 | }
346 |
347 | get step() {
348 | return this.time / this.duration
349 | }
350 |
351 | get reversed() {
352 | return this._reversed
353 | }
354 |
355 | set reversed(bool) {
356 | this._reversed = bool
357 | if (this.pausedTime) {
358 | this.time = this.pausedTime
359 | } else if (!this.isPlaying) {
360 | this.time = this.reversed ? this.duration : 0
361 | }
362 | this.setKeyframes(this.time)
363 | }
364 | }
365 |
366 | export default Animation
367 |
--------------------------------------------------------------------------------
/src/core.ts:
--------------------------------------------------------------------------------
1 | import Animation from './Animation'
2 |
3 | const _animations: Animation[] = []
4 | let _animationsLength = 0
5 |
6 | let _autoPlay = false
7 | let _rafId: number
8 |
9 | const update = (time: number) => {
10 | if (_autoPlay) {
11 | _rafId = requestAnimationFrame(update)
12 | }
13 | time = time !== undefined ? time : performance.now()
14 |
15 | for (let i = 0; i < _animationsLength; i++) {
16 | _animations[i].update(time)
17 | }
18 | }
19 |
20 | const autoPlay = (auto: boolean) => {
21 | _autoPlay = auto
22 | _autoPlay ? (_rafId = requestAnimationFrame(update)) : cancelAnimationFrame(_rafId)
23 | }
24 |
25 | function add(tween: any) {
26 | _animations.push(tween)
27 | _animationsLength = _animations.length
28 | }
29 |
30 | function remove(tween: any) {
31 | const i = _animations.indexOf(tween)
32 | if (i > -1) {
33 | _animations.splice(i, 1)
34 | _animationsLength = _animations.length
35 | }
36 | }
37 |
38 | export { update, autoPlay, add, remove }
39 |
--------------------------------------------------------------------------------
/src/effects/Blur.ts:
--------------------------------------------------------------------------------
1 | import Property from '../property/Property'
2 | import AnimatedProperty from '../property/AnimatedProperty'
3 |
4 | type FastBoxBlurProps = {
5 | radius: []
6 | }
7 |
8 | class Blur {
9 | private radius: Property
10 |
11 | constructor(data: FastBoxBlurProps) {
12 | this.radius = data.radius.length > 1 ? new AnimatedProperty(data.radius) : new Property(data.radius)
13 | }
14 |
15 | setBlur(ctx: CanvasRenderingContext2D, time: number, scale: number) {
16 | const blur = this.radius.getValue(time) * scale
17 | ctx.filter = `blur(${blur}px)`
18 | }
19 |
20 | setKeyframes(time: number) {
21 | this.radius.setKeyframes(time)
22 | }
23 |
24 | reset(reversed: boolean) {
25 | this.radius.reset(reversed)
26 | }
27 | }
28 |
29 | export default Blur
30 |
--------------------------------------------------------------------------------
/src/effects/DropShadow.ts:
--------------------------------------------------------------------------------
1 | import Property from '../property/Property'
2 | import AnimatedProperty from '../property/AnimatedProperty'
3 |
4 | type DropShadowProps = {
5 | color: []
6 | opacity: []
7 | direction: []
8 | distance: []
9 | softness: []
10 | }
11 |
12 | class DropShadow {
13 | private color: Property<[number, number, number]>
14 | private opacity: Property
15 | private direction: Property
16 | private distance: Property
17 | private softness: Property
18 |
19 | constructor(data: DropShadowProps) {
20 | this.color = data.color.length > 1 ? new AnimatedProperty(data.color) : new Property(data.color)
21 | this.opacity = data.opacity.length > 1 ? new AnimatedProperty(data.opacity) : new Property(data.opacity)
22 | this.direction = data.direction.length > 1 ? new AnimatedProperty(data.direction) : new Property(data.direction)
23 | this.distance = data.distance.length > 1 ? new AnimatedProperty(data.distance) : new Property(data.distance)
24 | this.softness = data.softness.length > 1 ? new AnimatedProperty(data.softness) : new Property(data.softness)
25 | }
26 |
27 | getColor(time: number) {
28 | const color = this.color.getValue(time)
29 | const opacity = this.opacity.getValue(time)
30 | return `rgba(${Math.round(color[0])}, ${Math.round(color[1])}, ${Math.round(color[2])}, ${opacity})`
31 | }
32 |
33 | setShadow(ctx: CanvasRenderingContext2D, time: number) {
34 | const color = this.getColor(time)
35 | const dist = this.distance.getValue(time)
36 | const blur = this.softness.getValue(time)
37 | ctx.shadowColor = color
38 | ctx.shadowOffsetX = dist
39 | ctx.shadowOffsetY = dist
40 | ctx.shadowBlur = blur
41 | }
42 |
43 | setKeyframes(time: number) {
44 | this.color.setKeyframes(time)
45 | this.opacity.setKeyframes(time)
46 | this.direction.setKeyframes(time)
47 | this.distance.setKeyframes(time)
48 | this.softness.setKeyframes(time)
49 | }
50 |
51 | reset(reversed: boolean) {
52 | this.color.reset(reversed)
53 | this.opacity.reset(reversed)
54 | this.direction.reset(reversed)
55 | this.distance.reset(reversed)
56 | this.softness.reset(reversed)
57 | }
58 | }
59 |
60 | export default DropShadow
61 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { update, autoPlay } from './core'
2 | import Animation from './Animation'
3 |
4 | export { Animation, update, autoPlay }
5 |
--------------------------------------------------------------------------------
/src/layers/BaseLayer.ts:
--------------------------------------------------------------------------------
1 | import Path, { PathProps } from '../objects/Path'
2 | import AnimatedPath from '../objects/AnimatedPath'
3 | import Transform, { TransformProps } from '../transform/Transform'
4 | import DropShadow from '../effects/DropShadow'
5 | import BlendingMode from '../property/BlendingMode'
6 | import Fill, { FillProps } from '../property/Fill'
7 | import GradientFill, { GradientFillProps } from '../property/GradientFill'
8 | import Stroke, { StrokeProps } from '../property/Stroke'
9 | import { TrimProps, TrimValues } from '../property/Trim'
10 | import Blur from '../effects/Blur'
11 |
12 | export type BaseLayerProps = {
13 | shapes: PathProps[]
14 | groups: []
15 | trim: TrimProps
16 | stroke: StrokeProps
17 | gradientFill: GradientFillProps
18 | fill: FillProps
19 | type: LayerType
20 | justification: CanvasTextAlign
21 | color: string
22 | font: string
23 | fontSize: number
24 | leading: number
25 | text: string
26 | timeRemapping: []
27 | source: string
28 | sourceID: string
29 | name: string
30 | index: number
31 | in?: number
32 | out: number
33 | transform: TransformProps
34 | parent?: any
35 | blendMode?: string
36 | masks: PathProps[]
37 | effects?: {
38 | blur: { radius: [] }
39 | dropShadow?: any
40 | }
41 | layers?: BaseLayerProps[]
42 | }
43 |
44 | export type LayerType = 'base' | 'vector' | 'image' | 'text' | 'comp' | 'null'
45 |
46 | class BaseLayer {
47 | private readonly blendMode?: BlendingMode
48 | private readonly dropShadow?: DropShadow
49 | private readonly blur?: Blur
50 | private readonly masks?: (AnimatedPath | Path)[]
51 |
52 | public type: LayerType = 'base'
53 | public name: string
54 |
55 | public index: number
56 | public in: number
57 | public out: number
58 | public transform: Transform
59 |
60 | public timeRemapping?: any
61 | public layers: BaseLayer[] = []
62 | public parent?: BaseLayer
63 | public parentIndex?: number
64 |
65 | constructor(data: BaseLayerProps) {
66 | this.name = data.name
67 | this.index = data.index
68 | this.in = data.in || 0
69 | this.out = data.out
70 | if (data.parent) this.parentIndex = data.parent
71 | if (data.blendMode) this.blendMode = new BlendingMode(data.blendMode)
72 | this.transform = new Transform(data.transform)
73 |
74 | if (data.effects?.dropShadow) {
75 | this.dropShadow = new DropShadow(data.effects.dropShadow)
76 | }
77 | if (data.effects?.blur) {
78 | this.blur = new Blur(data.effects.blur)
79 | }
80 |
81 | if (data.masks) {
82 | this.masks = data.masks.map((mask) => (mask.isAnimated ? new AnimatedPath(mask) : new Path(mask)))
83 | }
84 | }
85 |
86 | draw(
87 | ctx: CanvasRenderingContext2D,
88 | time: number,
89 | scale: number,
90 | parentFill?: Fill | GradientFill,
91 | parentStroke?: Stroke,
92 | parentTrim?: TrimValues
93 | ) {
94 | ctx.save()
95 |
96 | this.parent?.setParentTransform(ctx, time)
97 | this.blendMode?.setCompositeOperation(ctx)
98 |
99 | this.transform.update(ctx, time)
100 |
101 | this.dropShadow?.setShadow(ctx, time)
102 | this.blur?.setBlur(ctx, time, scale)
103 |
104 | if (this.masks) {
105 | ctx.beginPath()
106 | this.masks.forEach((mask) => mask.draw(ctx, time))
107 | ctx.clip()
108 | }
109 | }
110 |
111 | setParentTransform(ctx: CanvasRenderingContext2D, time: number) {
112 | this.parent?.setParentTransform(ctx, time)
113 | this.transform.update(ctx, time, false)
114 | }
115 |
116 | setKeyframes(time: number) {
117 | this.transform.setKeyframes(time)
118 | if (this.masks) this.masks.forEach((mask) => mask.setKeyframes(time))
119 | }
120 |
121 | reset(reversed: boolean) {
122 | this.transform.reset(reversed)
123 | if (this.masks) this.masks.forEach((mask) => mask.reset(reversed))
124 | }
125 | }
126 |
127 | export default BaseLayer
128 |
--------------------------------------------------------------------------------
/src/layers/CompLayer.ts:
--------------------------------------------------------------------------------
1 | import ImageLayer from './ImageLayer'
2 | import NullLayer from './NullLayer'
3 | import TextLayer from './TextLayer'
4 | import BaseLayer, { BaseLayerProps } from './BaseLayer'
5 | import VectorLayer from './VectorLayer'
6 | import Property from '../property/Property'
7 | import AnimatedProperty from '../property/AnimatedProperty'
8 | import { Gradients } from '../Animation'
9 |
10 | class CompLayer extends BaseLayer {
11 | public readonly type = 'comp'
12 |
13 | constructor(
14 | data: BaseLayerProps,
15 | comps: { [key: string]: BaseLayerProps },
16 | baseFont: string | undefined,
17 | gradients: Gradients,
18 | imageBasePath: string
19 | ) {
20 | super(data)
21 |
22 | const sourceID = data.sourceID
23 | const layers = comps && comps[sourceID] ? comps[sourceID].layers : null
24 |
25 | if (layers) {
26 | this.layers = layers
27 | .map((layer) => {
28 | switch (layer.type) {
29 | case 'vector':
30 | return new VectorLayer(layer, gradients)
31 | case 'image':
32 | return new ImageLayer(layer, imageBasePath)
33 | case 'text':
34 | return new TextLayer(layer, baseFont)
35 | case 'comp':
36 | return new CompLayer(layer, comps, baseFont, gradients, imageBasePath)
37 | case 'null':
38 | return new NullLayer(layer)
39 | }
40 | })
41 | .filter(Boolean) as BaseLayer[]
42 |
43 | this.layers.forEach((layer) => {
44 | layer.parent = this.layers.find((l) => l.index === layer.parentIndex)
45 | })
46 | }
47 |
48 | if (data.timeRemapping) {
49 | this.timeRemapping =
50 | data.timeRemapping.length > 1
51 | ? new AnimatedProperty(data.timeRemapping)
52 | : new Property(data.timeRemapping)
53 | }
54 | }
55 |
56 | draw(ctx: CanvasRenderingContext2D, time: number, scale: number) {
57 | super.draw(ctx, time, scale)
58 |
59 | if (this.layers) {
60 | let internalTime = time - this.in
61 | if (this.timeRemapping) internalTime = this.timeRemapping.getValue(internalTime)
62 | this.layers.forEach((layer) => {
63 | if (internalTime >= layer.in && internalTime <= layer.out) {
64 | layer.draw(ctx, internalTime, scale)
65 | }
66 | })
67 | }
68 |
69 | ctx.restore()
70 | }
71 |
72 | setParentTransform(ctx: CanvasRenderingContext2D, time: number) {
73 | super.setParentTransform(ctx, time)
74 | const internalTime = time - this.in
75 | if (this.layers) this.layers.forEach((layer) => layer.setParentTransform(ctx, internalTime))
76 | }
77 |
78 | setKeyframes(time: number) {
79 | super.setKeyframes(time)
80 | const internalTime = time - this.in
81 | if (this.timeRemapping) this.timeRemapping.setKeyframes(internalTime)
82 | if (this.layers) this.layers.forEach((layer) => layer.setKeyframes(internalTime))
83 | }
84 |
85 | reset(reversed: boolean) {
86 | super.reset(reversed)
87 | if (this.timeRemapping) this.timeRemapping.reset(reversed)
88 | if (this.layers) this.layers.forEach((layer) => layer.reset(reversed))
89 | }
90 | }
91 |
92 | export default CompLayer
93 |
--------------------------------------------------------------------------------
/src/layers/ImageLayer.ts:
--------------------------------------------------------------------------------
1 | import BaseLayer from './BaseLayer'
2 | import { BaseLayerProps } from './BaseLayer'
3 |
4 | class ImageLayer extends BaseLayer {
5 | private readonly source: string
6 | private isLoaded = false
7 | private img?: HTMLImageElement
8 |
9 | constructor(data: BaseLayerProps, imageBasePath: string) {
10 | super(data)
11 | this.source = `${imageBasePath}${data.source}`
12 | }
13 |
14 | preload() {
15 | return new Promise((resolve) => {
16 | this.img = new Image()
17 | this.img.onload = () => {
18 | this.isLoaded = true
19 | resolve()
20 | }
21 | this.img.src = this.source
22 | })
23 | }
24 |
25 | draw(ctx: CanvasRenderingContext2D, time: number, scale: number) {
26 | super.draw(ctx, time, scale)
27 |
28 | if (this.img) {
29 | ctx.drawImage(this.img, 0, 0)
30 | }
31 |
32 | ctx.restore()
33 | }
34 | }
35 |
36 | export default ImageLayer
37 |
--------------------------------------------------------------------------------
/src/layers/NullLayer.ts:
--------------------------------------------------------------------------------
1 | import BaseLayer from './BaseLayer'
2 |
3 | class NullLayer extends BaseLayer {
4 | draw(ctx: CanvasRenderingContext2D, time: number, scale: number) {
5 | super.draw(ctx, time, scale)
6 | ctx.restore()
7 | }
8 | }
9 |
10 | export default NullLayer
11 |
--------------------------------------------------------------------------------
/src/layers/TextLayer.ts:
--------------------------------------------------------------------------------
1 | import BaseLayer from './BaseLayer'
2 | import { BaseLayerProps } from './BaseLayer'
3 |
4 | class TextLayer extends BaseLayer {
5 | private readonly text: string
6 | private readonly leading: number
7 | private readonly fontSize: number
8 | private readonly font: string
9 | private readonly color: string
10 | private readonly justification: CanvasTextAlign
11 | private readonly baseFont?: string
12 |
13 | constructor(data: BaseLayerProps, baseFont?: string) {
14 | super(data)
15 | this.text = data.text
16 | this.leading = data.leading
17 | this.fontSize = data.fontSize
18 | this.font = data.font
19 | this.color = data.color
20 | this.justification = data.justification
21 | this.baseFont = baseFont
22 | }
23 |
24 | draw(ctx: CanvasRenderingContext2D, time: number, scale: number) {
25 | super.draw(ctx, time, scale)
26 |
27 | ctx.textAlign = this.justification
28 | ctx.font = `${this.fontSize}px ${this.baseFont || this.font}`
29 | ctx.fillStyle = `rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})`
30 | for (let j = 0; j < this.text.length; j++) {
31 | ctx.fillText(this.text[j], 0, j * this.leading)
32 | }
33 |
34 | ctx.restore()
35 | }
36 | }
37 |
38 | export default TextLayer
39 |
--------------------------------------------------------------------------------
/src/layers/VectorLayer.ts:
--------------------------------------------------------------------------------
1 | import AnimatedPath from '../objects/AnimatedPath'
2 | import Ellipse from '../objects/Ellipse'
3 | import Path from '../objects/Path'
4 | import Polystar from '../objects/Polystar'
5 | import Rect from '../objects/Rect'
6 | import Fill from '../property/Fill'
7 | import GradientFill from '../property/GradientFill'
8 | import Stroke from '../property/Stroke'
9 | import Trim, { TrimValues } from '../property/Trim'
10 | import BaseLayer, { BaseLayerProps } from './BaseLayer'
11 | import { Gradients } from '../Animation'
12 |
13 | class VectorLayer extends BaseLayer {
14 | private fill?: Fill | GradientFill
15 | private stroke?: Stroke
16 | private trim?: Trim
17 | private groups?: VectorLayer[]
18 | private shapes?: (Path | Rect | Ellipse | Polystar)[]
19 |
20 | constructor(data: BaseLayerProps, gradients: Gradients) {
21 | super(data)
22 |
23 | if (data.fill) this.fill = new Fill(data.fill)
24 | if (data.gradientFill) this.fill = new GradientFill(data.gradientFill, gradients)
25 | if (data.stroke) this.stroke = new Stroke(data.stroke)
26 | if (data.trim) this.trim = new Trim(data.trim)
27 |
28 | if (data.groups) {
29 | this.groups = data.groups.map((group) => new VectorLayer(group, gradients))
30 | }
31 |
32 | if (data.shapes) {
33 | this.shapes = data.shapes
34 | .map((shape) => {
35 | if (shape.type === 'path') {
36 | return shape.isAnimated ? new AnimatedPath(shape) : new Path(shape)
37 | } else if (shape.type === 'rect') {
38 | return new Rect(shape)
39 | } else if (shape.type === 'ellipse') {
40 | return new Ellipse(shape)
41 | } else if (shape.type === 'polystar') {
42 | return new Polystar(shape)
43 | }
44 | })
45 | .filter(Boolean) as (Path | Rect | Ellipse | Polystar)[]
46 | }
47 | }
48 |
49 | draw(
50 | ctx: CanvasRenderingContext2D,
51 | time: number,
52 | scale: number,
53 | parentFill: Fill | GradientFill,
54 | parentStroke: Stroke,
55 | parentTrim: TrimValues
56 | ) {
57 | super.draw(ctx, time, scale)
58 |
59 | const fill = this.fill || parentFill
60 | const stroke = this.stroke || parentStroke
61 | const trimValues = this.trim ? this.trim.getTrim(time) : parentTrim
62 |
63 | if (fill) fill.update(ctx, time)
64 | if (stroke) stroke.update(ctx, time)
65 |
66 | ctx.beginPath()
67 | if (this.shapes) {
68 | this.shapes.forEach((shape) => shape.draw(ctx, time, trimValues))
69 | if (this.shapes[this.shapes.length - 1].closed) {
70 | // ctx.closePath();
71 | }
72 | }
73 |
74 | if (fill) ctx.fill()
75 | if (stroke) ctx.stroke()
76 |
77 | if (this.groups) this.groups.forEach((group) => group.draw(ctx, time, scale, fill, stroke, trimValues))
78 | ctx.restore()
79 | }
80 |
81 | setKeyframes(time: number) {
82 | super.setKeyframes(time)
83 |
84 | if (this.shapes) this.shapes.forEach((shape) => shape.setKeyframes(time))
85 | if (this.groups) this.groups.forEach((group) => group.setKeyframes(time))
86 |
87 | if (this.fill) this.fill.setKeyframes(time)
88 | if (this.stroke) this.stroke.setKeyframes(time)
89 | if (this.trim) this.trim.setKeyframes(time)
90 | }
91 |
92 | reset(reversed: boolean) {
93 | super.reset(reversed)
94 |
95 | if (this.shapes) this.shapes.forEach((shape) => shape.reset(reversed))
96 | if (this.groups) this.groups.forEach((group) => group.reset(reversed))
97 |
98 | if (this.fill) this.fill.reset(reversed)
99 | if (this.stroke) this.stroke.reset(reversed)
100 | if (this.trim) this.trim.reset(reversed)
101 | }
102 | }
103 |
104 | export default VectorLayer
105 |
--------------------------------------------------------------------------------
/src/objects/AnimatedPath.ts:
--------------------------------------------------------------------------------
1 | import Path, { type PathProps, Vertex } from './Path'
2 | import BezierEasing from '../utils/BezierEasing'
3 | import { Frame } from '../property/Property'
4 |
5 | class AnimatedPath extends Path {
6 | private frameCount: number
7 |
8 | private nextFrame: Frame
9 | private lastFrame: Frame
10 | private easing?: any | null
11 | private finished = false
12 | private started = false
13 | private pointer = 0
14 | private verticesCount: number
15 |
16 | constructor(data: PathProps) {
17 | super(data)
18 | this.frameCount = this.frames.length
19 | this.verticesCount = this.frames[0].v?.length
20 |
21 | //todo check
22 | this.nextFrame = this.frames[1]
23 | this.lastFrame = this.frames[0]
24 | }
25 |
26 | getValue(time: number): Omit, 't'> {
27 | if (this.finished && time >= this.nextFrame.t) {
28 | return this.nextFrame
29 | } else if (!this.started && time <= this.lastFrame.t) {
30 | return this.lastFrame
31 | } else {
32 | this.started = true
33 | this.finished = false
34 | if (time > this.nextFrame.t) {
35 | if (this.pointer + 1 === this.frameCount) {
36 | this.finished = true
37 | } else {
38 | this.pointer++
39 | this.lastFrame = this.frames[this.pointer - 1]
40 | this.nextFrame = this.frames[this.pointer]
41 | this.onKeyframeChange()
42 | }
43 | } else if (time < this.lastFrame.t) {
44 | if (this.pointer < 2) {
45 | this.started = false
46 | } else {
47 | this.pointer--
48 | this.lastFrame = this.frames[this.pointer - 1]
49 | this.nextFrame = this.frames[this.pointer]
50 | this.onKeyframeChange()
51 | }
52 | }
53 | return this.getValueAtTime(time)
54 | }
55 | }
56 |
57 | setKeyframes(time: number) {
58 | if (time < this.frames[0].t) {
59 | this.pointer = 1
60 | this.nextFrame = this.frames[this.pointer]
61 | this.lastFrame = this.frames[this.pointer - 1]
62 | this.onKeyframeChange()
63 | return
64 | }
65 |
66 | if (time > this.frames[this.frameCount - 1].t) {
67 | this.pointer = this.frameCount - 1
68 | this.nextFrame = this.frames[this.pointer]
69 | this.lastFrame = this.frames[this.pointer - 1]
70 | this.onKeyframeChange()
71 | return
72 | }
73 |
74 | for (let i = 1; i < this.frameCount; i++) {
75 | if (time >= this.frames[i - 1].t && time <= this.frames[i].t) {
76 | this.pointer = i
77 | this.lastFrame = this.frames[i - 1]
78 | this.nextFrame = this.frames[i]
79 | this.onKeyframeChange()
80 | return
81 | }
82 | }
83 | }
84 |
85 | onKeyframeChange() {
86 | this.setEasing()
87 | }
88 |
89 | lerp(a: number, b: number, t: number) {
90 | return a + t * (b - a)
91 | }
92 |
93 | setEasing() {
94 | if (this.lastFrame.easeOut && this.nextFrame.easeIn) {
95 | this.easing = new (BezierEasing as any)(
96 | this.lastFrame.easeOut[0],
97 | this.lastFrame.easeOut[1],
98 | this.nextFrame.easeIn[0],
99 | this.nextFrame.easeIn[1]
100 | )
101 | } else {
102 | this.easing = null
103 | }
104 | }
105 |
106 | getValueAtTime(time: number): Omit, 't'> {
107 | const delta = time - this.lastFrame.t
108 | const duration = this.nextFrame.t - this.lastFrame.t
109 | let elapsed = delta / duration
110 | if (elapsed > 1) elapsed = 1
111 | else if (elapsed < 0) elapsed = 0
112 | else if (this.easing) elapsed = this.easing(elapsed)
113 | const actualVertices: Vertex[] = []
114 | const actualLengths = []
115 |
116 | for (let i = 0; i < this.verticesCount; i++) {
117 | const cp1x = this.lerp(this.lastFrame.v[i][0], this.nextFrame.v[i][0], elapsed)
118 | const cp1y = this.lerp(this.lastFrame.v[i][1], this.nextFrame.v[i][1], elapsed)
119 | const cp2x = this.lerp(this.lastFrame.v[i][2], this.nextFrame.v[i][2], elapsed)
120 | const cp2y = this.lerp(this.lastFrame.v[i][3], this.nextFrame.v[i][3], elapsed)
121 | const x = this.lerp(this.lastFrame.v[i][4], this.nextFrame.v[i][4], elapsed)
122 | const y = this.lerp(this.lastFrame.v[i][5], this.nextFrame.v[i][5], elapsed)
123 |
124 | actualVertices.push([cp1x, cp1y, cp2x, cp2y, x, y])
125 | }
126 |
127 | for (let j = 0; j < this.verticesCount - 1; j++) {
128 | actualLengths.push(this.lerp(this.lastFrame.len[j], this.nextFrame.len[j], elapsed))
129 | }
130 |
131 | return {
132 | v: actualVertices,
133 | len: actualLengths,
134 | }
135 | }
136 |
137 | reset(reversed: boolean) {
138 | this.finished = false
139 | this.started = false
140 | this.pointer = reversed ? this.frameCount - 1 : 1
141 | this.nextFrame = this.frames[this.pointer]
142 | this.lastFrame = this.frames[this.pointer - 1]
143 | this.onKeyframeChange()
144 | }
145 | }
146 |
147 | export default AnimatedPath
148 |
--------------------------------------------------------------------------------
/src/objects/Ellipse.ts:
--------------------------------------------------------------------------------
1 | import Property from '../property/Property'
2 | import AnimatedProperty from '../property/AnimatedProperty'
3 | import Path, { type PathProps, Vertex } from './Path'
4 | import { TrimValues } from '../property/Trim'
5 |
6 | class Ellipse extends Path {
7 | private readonly size: Property<[number, number]>
8 | private readonly position?: Property<[number, number]>
9 |
10 | constructor(data: PathProps) {
11 | super(data)
12 | this.closed = true
13 |
14 | this.size = data.size.length > 1 ? new AnimatedProperty(data.size) : new Property(data.size)
15 |
16 | if (data.position) {
17 | this.position = data.position.length > 1 ? new AnimatedProperty(data.position) : new Property(data.position)
18 | }
19 | }
20 |
21 | draw(ctx: CanvasRenderingContext2D, time: number, parentTrim: TrimValues) {
22 | const size = this.size.getValue(time)
23 | const position = this.position ? this.position.getValue(time) : [0, 0]
24 |
25 | let i
26 | let j
27 | const w = size[0] / 2
28 | const h = size[1] / 2
29 | const x = position[0] - w
30 | const y = position[1] - h
31 | const ow = w * 0.5522848
32 | const oh = h * 0.5522848
33 |
34 | const vertices: Vertex[] = [
35 | [x + w + ow, y, x + w - ow, y, x + w, y],
36 | [x + w + w, y + h + oh, x + w + w, y + h - oh, x + w + w, y + h],
37 | [x + w - ow, y + h + h, x + w + ow, y + h + h, x + w, y + h + h],
38 | [x, y + h - oh, x, y + h + oh, x, y + h],
39 | ]
40 |
41 | if (parentTrim) {
42 | let tv
43 | const len = w + h
44 |
45 | const trim = this.getTrimValues(parentTrim)
46 |
47 | if (trim === null) {
48 | return
49 | }
50 |
51 | for (i = 0; i < 4; i++) {
52 | j = i + 1
53 | if (j > 3) j = 0
54 | if (i > trim.startIndex && i < trim.endIndex) {
55 | ctx.bezierCurveTo(
56 | vertices[i][0],
57 | vertices[i][1],
58 | vertices[j][2],
59 | vertices[j][3],
60 | vertices[j][4],
61 | vertices[j][5]
62 | )
63 | } else if (i === trim.startIndex && i === trim.endIndex) {
64 | tv = this.trim(vertices[i], vertices[j], trim.start, trim.end, len)
65 | ctx.moveTo(tv.start[4], tv.start[5])
66 | ctx.bezierCurveTo(tv.start[0], tv.start[1], tv.end[2], tv.end[3], tv.end[4], tv.end[5])
67 | } else if (i === trim.startIndex) {
68 | tv = this.trim(vertices[i], vertices[j], trim.start, 1, len)
69 | ctx.moveTo(tv.start[4], tv.start[5])
70 | ctx.bezierCurveTo(tv.start[0], tv.start[1], tv.end[2], tv.end[3], tv.end[4], tv.end[5])
71 | } else if (i === trim.endIndex) {
72 | tv = this.trim(vertices[i], vertices[j], 0, trim.end, len)
73 | ctx.bezierCurveTo(tv.start[0], tv.start[1], tv.end[2], tv.end[3], tv.end[4], tv.end[5])
74 | }
75 | }
76 | } else {
77 | ctx.moveTo(vertices[0][4], vertices[0][5])
78 | for (i = 0; i < 4; i++) {
79 | j = i + 1
80 | if (j > 3) j = 0
81 | ctx.bezierCurveTo(
82 | vertices[i][0],
83 | vertices[i][1],
84 | vertices[j][2],
85 | vertices[j][3],
86 | vertices[j][4],
87 | vertices[j][5]
88 | )
89 | }
90 | }
91 | }
92 |
93 | getTrimValues(
94 | trim: TrimValues
95 | ): { start: number; end: number; startIndex: number; endIndex: number; looped: boolean } | null {
96 | if (trim === null) {
97 | return null
98 | }
99 | const startIndex = Math.floor(trim.start * 4)
100 | const endIndex = Math.floor(trim.end * 4)
101 | const start = (trim.start - startIndex * 0.25) * 4
102 | const end = (trim.end - endIndex * 0.25) * 4
103 |
104 | return {
105 | startIndex,
106 | endIndex,
107 | start,
108 | end,
109 | looped: false,
110 | }
111 | }
112 |
113 | setKeyframes(time: number) {
114 | this.size.setKeyframes(time)
115 | if (this.position) this.position.setKeyframes(time)
116 | }
117 |
118 | reset(reversed: boolean) {
119 | this.size.reset(reversed)
120 | if (this.position) this.position.reset(reversed)
121 | }
122 | }
123 |
124 | export default Ellipse
125 |
--------------------------------------------------------------------------------
/src/objects/Group.ts:
--------------------------------------------------------------------------------
1 | import Stroke, { StrokeProps } from '../property/Stroke'
2 | import Path, { PathProps } from './Path'
3 | import Rect from './Rect'
4 | import Ellipse from './Ellipse'
5 | import Polystar from './Polystar'
6 | import AnimatedPath from './AnimatedPath'
7 | import Fill, { FillProps } from '../property/Fill'
8 | import GradientFill, { GradientFillProps } from '../property/GradientFill'
9 | import Transform, { TransformProps } from '../transform/Transform'
10 | import Trim, { TrimProps, TrimValues } from '../property/Trim'
11 | import { Gradients } from '../Animation'
12 |
13 | type GroupProps = {
14 | transform: TransformProps
15 | shapes: PathProps[]
16 | groups: GroupProps[]
17 | trim: TrimProps
18 | stroke: StrokeProps
19 | gradientFill: GradientFillProps
20 | fill: FillProps
21 | index: number
22 | }
23 |
24 | class Group {
25 | private index: number
26 | private fill?: Fill | GradientFill
27 | private stroke?: Stroke
28 | private trim?: Trim
29 | private groups?: Group[]
30 | private shapes?: (Path | Rect | Ellipse | Polystar)[]
31 | private transform: Transform
32 |
33 | constructor(data: GroupProps, gradients: Gradients) {
34 | this.index = data.index
35 |
36 | if (data.fill) this.fill = new Fill(data.fill)
37 | if (data.gradientFill) this.fill = new GradientFill(data.gradientFill, gradients)
38 | if (data.stroke) this.stroke = new Stroke(data.stroke)
39 | if (data.trim) this.trim = new Trim(data.trim)
40 |
41 | this.transform = new Transform(data.transform)
42 |
43 | if (data.groups) {
44 | this.groups = data.groups.map((group) => new Group(group, gradients))
45 | }
46 |
47 | if (data.shapes) {
48 | this.shapes = data.shapes
49 | .map((shape) => {
50 | if (shape.type === 'path') {
51 | return shape.isAnimated ? new AnimatedPath(shape) : new Path(shape)
52 | } else if (shape.type === 'rect') {
53 | return new Rect(shape)
54 | } else if (shape.type === 'ellipse') {
55 | return new Ellipse(shape)
56 | } else if (shape.type === 'polystar') {
57 | return new Polystar(shape)
58 | }
59 | })
60 | .filter(Boolean) as (Path | Rect | Ellipse | Polystar)[]
61 | }
62 | }
63 |
64 | draw(
65 | ctx: CanvasRenderingContext2D,
66 | time: number,
67 | parentFill: Fill | GradientFill,
68 | parentStroke: Stroke,
69 | parentTrim: TrimValues
70 | ) {
71 | ctx.save()
72 |
73 | //TODO check if color/stroke is changing over time
74 | const fill = this.fill || parentFill
75 | const stroke = this.stroke || parentStroke
76 | const trimValues = this.trim ? this.trim.getTrim(time) : parentTrim
77 |
78 | if (fill) fill.update(ctx, time)
79 | if (stroke) stroke.update(ctx, time)
80 |
81 | this.transform.update(ctx, time)
82 |
83 | ctx.beginPath()
84 | if (this.shapes) {
85 | this.shapes.forEach((shape) => shape.draw(ctx, time, trimValues))
86 | if (this.shapes[this.shapes.length - 1].closed) {
87 | // ctx.closePath();
88 | }
89 | }
90 |
91 | //TODO get order
92 | if (fill) ctx.fill()
93 | if (stroke) ctx.stroke()
94 |
95 | if (this.groups) this.groups.forEach((group) => group.draw(ctx, time, fill, stroke, trimValues))
96 |
97 | ctx.restore()
98 | }
99 |
100 | setKeyframes(time: number) {
101 | this.transform.setKeyframes(time)
102 |
103 | if (this.shapes) this.shapes.forEach((shape) => shape.setKeyframes(time))
104 | if (this.groups) this.groups.forEach((group) => group.setKeyframes(time))
105 |
106 | if (this.fill) this.fill.setKeyframes(time)
107 | if (this.stroke) this.stroke.setKeyframes(time)
108 | if (this.trim) this.trim.setKeyframes(time)
109 | }
110 |
111 | reset(reversed: boolean) {
112 | this.transform.reset(reversed)
113 |
114 | if (this.shapes) this.shapes.forEach((shape) => shape.reset(reversed))
115 | if (this.groups) this.groups.forEach((group) => group.reset(reversed))
116 |
117 | if (this.fill) this.fill.reset(reversed)
118 | if (this.stroke) this.stroke.reset(reversed)
119 | if (this.trim) this.trim.reset(reversed)
120 | }
121 | }
122 |
123 | export default Group
124 |
--------------------------------------------------------------------------------
/src/objects/Path.ts:
--------------------------------------------------------------------------------
1 | import Bezier from '../utils/Bezier'
2 | import { type Frame } from '../property/Property'
3 | import { type TrimValues } from '../property/Trim'
4 |
5 | export type PathProps = {
6 | position?: []
7 | size: []
8 | closed: boolean
9 | frames: Frame[]
10 | isAnimated?: boolean
11 | type: 'path' | 'rect' | 'ellipse' | 'polystar'
12 | outerRoundness?: []
13 | innerRoundness?: []
14 | rotation?: []
15 | outerRadius: []
16 | innerRadius: []
17 | points: []
18 | starType: number
19 | }
20 |
21 | export type Vertex = [number, number, number, number, number, number]
22 |
23 | class Path {
24 | public closed: boolean
25 | public frames: Frame[]
26 | private bezier?: Bezier
27 |
28 | constructor(data: PathProps) {
29 | this.closed = data.closed
30 | this.frames = data.frames
31 | }
32 |
33 | draw(ctx: CanvasRenderingContext2D, time: number, trim?: TrimValues) {
34 | const frame = this.getValue(time)
35 | trim && (trim.start !== 0 || trim.end !== 1) ? this.drawTrimmed(frame, ctx, trim) : this.drawNormal(frame, ctx)
36 | }
37 |
38 | drawNormal(frame: { v: Vertex[]; len: number[] }, ctx: CanvasRenderingContext2D) {
39 | const vertices = frame.v
40 | const numVertices = this.closed ? vertices.length : vertices.length - 1
41 | let lastVertex = null
42 | let nextVertex = null
43 |
44 | for (let i = 1; i <= numVertices; i++) {
45 | lastVertex = vertices[i - 1]
46 | nextVertex = vertices[i] ? vertices[i] : vertices[0]
47 | if (i === 1) ctx.moveTo(lastVertex[4], lastVertex[5])
48 | ctx.bezierCurveTo(lastVertex[0], lastVertex[1], nextVertex[2], nextVertex[3], nextVertex[4], nextVertex[5])
49 | }
50 |
51 | if (this.closed) {
52 | //todo check
53 | if (!nextVertex) {
54 | debugger
55 | return
56 | }
57 | ctx.bezierCurveTo(
58 | nextVertex[0],
59 | nextVertex[1],
60 | vertices[0][2],
61 | vertices[0][3],
62 | vertices[0][4],
63 | vertices[0][5]
64 | )
65 | ctx.closePath()
66 | }
67 | }
68 |
69 | drawTrimmed(frame: { v: Vertex[]; len: number[] }, ctx: CanvasRenderingContext2D, trim: TrimValues) {
70 | if (trim?.start === trim?.end) return
71 |
72 | const vertices = frame.v
73 | const numVertices = this.closed ? vertices.length : vertices.length - 1
74 |
75 | let nextVertex: Vertex
76 | let lastVertex: Vertex
77 |
78 | const trimValues = this.getTrimValues(trim, frame)
79 |
80 | if (trimValues === null) {
81 | return
82 | }
83 |
84 | const { start, end, endIndex, startIndex, looped } = trimValues
85 | if (looped && this.closed) {
86 | let index = startIndex
87 | let firstRound = true
88 |
89 | for (let i = 1; i <= numVertices + 1; i++) {
90 | lastVertex = vertices[index]
91 | nextVertex = vertices[index + 1] ? vertices[index + 1] : vertices[0]
92 | const length = frame.len[index]
93 |
94 | if (index === startIndex && firstRound) {
95 | firstRound = false
96 | const tv = this.trim(lastVertex, nextVertex, start, 1, length)
97 | ctx.moveTo(tv.start[4], tv.start[5])
98 | ctx.bezierCurveTo(tv.start[0], tv.start[1], tv.end[2], tv.end[3], tv.end[4], tv.end[5])
99 | } else if (index === startIndex && index === endIndex) {
100 | const tv = this.trim(lastVertex, nextVertex, 0, end, length)
101 | ctx.bezierCurveTo(tv.start[0], tv.start[1], tv.end[2], tv.end[3], tv.end[4], tv.end[5])
102 | } else if (index === endIndex) {
103 | const tv = this.trim(lastVertex, nextVertex, 0, end, length)
104 | ctx.bezierCurveTo(tv.start[0], tv.start[1], tv.end[2], tv.end[3], tv.end[4], tv.end[5])
105 | } else if (index < endIndex || index > startIndex) {
106 | ctx.bezierCurveTo(
107 | lastVertex[0],
108 | lastVertex[1],
109 | nextVertex[2],
110 | nextVertex[3],
111 | nextVertex[4],
112 | nextVertex[5]
113 | )
114 | }
115 |
116 | index + 1 < numVertices ? index++ : (index = 0)
117 | }
118 | } else if (looped && !this.closed) {
119 | for (let i = 1; i <= numVertices; i++) {
120 | const index = i - 1
121 | lastVertex = vertices[index]
122 | nextVertex = vertices[index + 1] ? vertices[index + 1] : vertices[0]
123 | const length = frame.len[index]
124 |
125 | if (index === startIndex && index === endIndex) {
126 | const tv1 = this.trim(lastVertex, nextVertex, 0, end, length)
127 | ctx.bezierCurveTo(tv1.start[0], tv1.start[1], tv1.end[2], tv1.end[3], tv1.end[4], tv1.end[5])
128 |
129 | const tv2 = this.trim(lastVertex, nextVertex, start, 1, length)
130 | ctx.moveTo(tv2.start[4], tv2.start[5])
131 | ctx.bezierCurveTo(tv2.start[0], tv2.start[1], tv2.end[2], tv2.end[3], tv2.end[4], tv2.end[5])
132 | } else if (index === startIndex) {
133 | const tv = this.trim(lastVertex, nextVertex, start, 1, length)
134 | ctx.moveTo(tv.start[4], tv.start[5])
135 | ctx.bezierCurveTo(tv.start[0], tv.start[1], tv.end[2], tv.end[3], tv.end[4], tv.end[5])
136 | } else if (index === endIndex) {
137 | const tv = this.trim(lastVertex, nextVertex, 0, end, length)
138 | ctx.bezierCurveTo(tv.start[0], tv.start[1], tv.end[2], tv.end[3], tv.end[4], tv.end[5])
139 | } else if (index < endIndex || index > startIndex) {
140 | ctx.bezierCurveTo(
141 | lastVertex[0],
142 | lastVertex[1],
143 | nextVertex[2],
144 | nextVertex[3],
145 | nextVertex[4],
146 | nextVertex[5]
147 | )
148 | }
149 | }
150 | } else {
151 | for (let i = 1; i <= numVertices; i++) {
152 | const index = i - 1
153 | lastVertex = vertices[i - 1]
154 | nextVertex = vertices[i] ? vertices[i] : vertices[0]
155 | if (index === startIndex && index === endIndex) {
156 | const tv = this.trim(lastVertex, nextVertex, start, end, length)
157 | ctx.moveTo(tv.start[4], tv.start[5])
158 | ctx.bezierCurveTo(tv.start[0], tv.start[1], tv.end[2], tv.end[3], tv.end[4], tv.end[5])
159 | } else if (index === startIndex) {
160 | const tv = this.trim(lastVertex, nextVertex, start, 1, length)
161 | ctx.moveTo(tv.start[4], tv.start[5])
162 | ctx.bezierCurveTo(tv.start[0], tv.start[1], tv.end[2], tv.end[3], tv.end[4], tv.end[5])
163 | } else if (index === endIndex) {
164 | const tv = this.trim(lastVertex, nextVertex, 0, end, length)
165 | ctx.bezierCurveTo(tv.start[0], tv.start[1], tv.end[2], tv.end[3], tv.end[4], tv.end[5])
166 | } else if (index > startIndex && index < endIndex) {
167 | ctx.bezierCurveTo(
168 | lastVertex[0],
169 | lastVertex[1],
170 | nextVertex[2],
171 | nextVertex[3],
172 | nextVertex[4],
173 | nextVertex[5]
174 | )
175 | }
176 | }
177 | }
178 | }
179 |
180 | getValue(time: number): { v: Vertex[]; len: number[] } {
181 | return { v: this.frames[0].v as Vertex[], len: this.frames[0].len }
182 | }
183 |
184 | getTrimValues(
185 | trim: TrimValues,
186 | frame: { v: Vertex[]; len: number[] }
187 | ): { start: number; end: number; endIndex: number; startIndex: number; looped: boolean } | null {
188 | const actualTrim = {
189 | startIndex: 0,
190 | endIndex: 0,
191 | start: 0,
192 | end: 0,
193 | looped: false,
194 | }
195 |
196 | if (trim === null) {
197 | return null
198 | }
199 |
200 | if (trim.start === 0 && trim.end === 1) {
201 | return null
202 | }
203 |
204 | const totalLen = this.sumArray(frame.len)
205 | let trimStartAtLength = totalLen * trim.start
206 |
207 | for (let i = 0; i < frame.len.length; i++) {
208 | if (trimStartAtLength === 0 || trimStartAtLength < frame.len[i]) {
209 | actualTrim.startIndex = i
210 | actualTrim.start = trimStartAtLength / frame.len[i]
211 | break
212 | }
213 | trimStartAtLength -= frame.len[i]
214 | }
215 |
216 | let trimEndAtLength = totalLen * trim.end
217 |
218 | if (trim.end === 1) {
219 | actualTrim.endIndex = frame.len.length
220 | actualTrim.end = 1
221 | return actualTrim
222 | }
223 |
224 | for (let i = 0; i < frame.len.length; i++) {
225 | if (trimEndAtLength === 0 || trimEndAtLength < frame.len[i]) {
226 | actualTrim.endIndex = i
227 | actualTrim.end = trimEndAtLength / frame.len[i]
228 | break
229 | }
230 | trimEndAtLength -= frame.len[i]
231 | }
232 |
233 | actualTrim.looped =
234 | actualTrim.startIndex > actualTrim.endIndex ||
235 | (actualTrim.startIndex === actualTrim.endIndex && actualTrim.start >= actualTrim.end)
236 |
237 | return actualTrim
238 | }
239 |
240 | trim(lastVertex: Vertex, nextVertex: Vertex, from: number, to: number, len: number) {
241 | const values = {
242 | start: lastVertex,
243 | end: nextVertex,
244 | }
245 |
246 | if (from === 0 && to === 1) {
247 | return values
248 | }
249 |
250 | if (
251 | this.isStraight(
252 | lastVertex[4],
253 | lastVertex[5],
254 | lastVertex[0],
255 | lastVertex[1],
256 | nextVertex[2],
257 | nextVertex[3],
258 | nextVertex[4],
259 | nextVertex[5]
260 | )
261 | ) {
262 | values.start = [
263 | this.lerp(lastVertex[0], nextVertex[0], from),
264 | this.lerp(lastVertex[1], nextVertex[1], from),
265 | this.lerp(lastVertex[2], nextVertex[2], from),
266 | this.lerp(lastVertex[3], nextVertex[3], from),
267 | this.lerp(lastVertex[4], nextVertex[4], from),
268 | this.lerp(lastVertex[5], nextVertex[5], from),
269 | ]
270 |
271 | values.end = [
272 | this.lerp(lastVertex[0], nextVertex[0], to),
273 | this.lerp(lastVertex[1], nextVertex[1], to),
274 | this.lerp(lastVertex[2], nextVertex[2], to),
275 | this.lerp(lastVertex[3], nextVertex[3], to),
276 | this.lerp(lastVertex[4], nextVertex[4], to),
277 | this.lerp(lastVertex[5], nextVertex[5], to),
278 | ]
279 |
280 | return values
281 | } else {
282 | this.bezier = new Bezier([
283 | lastVertex[4],
284 | lastVertex[5],
285 | lastVertex[0],
286 | lastVertex[1],
287 | nextVertex[2],
288 | nextVertex[3],
289 | nextVertex[4],
290 | nextVertex[5],
291 | ])
292 | this.bezier.getLength(len)
293 | from = this.bezier.map(from)
294 | to = this.bezier.map(to)
295 | to = (to - from) / (1 - from)
296 |
297 | let e1 = [this.lerp(lastVertex[4], lastVertex[0], from), this.lerp(lastVertex[5], lastVertex[1], from)]
298 | let f1 = [this.lerp(lastVertex[0], nextVertex[2], from), this.lerp(lastVertex[1], nextVertex[3], from)]
299 | let g1 = [this.lerp(nextVertex[2], nextVertex[4], from), this.lerp(nextVertex[3], nextVertex[5], from)]
300 | let h1 = [this.lerp(e1[0], f1[0], from), this.lerp(e1[1], f1[1], from)]
301 | let j1 = [this.lerp(f1[0], g1[0], from), this.lerp(f1[1], g1[1], from)]
302 | let k1 = [this.lerp(h1[0], j1[0], from), this.lerp(h1[1], j1[1], from)]
303 |
304 | let startVertex = [j1[0], j1[1], h1[0], h1[1], k1[0], k1[1]]
305 | let endVertex = [nextVertex[0], nextVertex[1], g1[0], g1[1], nextVertex[4], nextVertex[5]]
306 |
307 | let e2 = [this.lerp(startVertex[4], startVertex[0], to), this.lerp(startVertex[5], startVertex[1], to)]
308 | let f2 = [this.lerp(startVertex[0], endVertex[2], to), this.lerp(startVertex[1], endVertex[3], to)]
309 | let g2 = [this.lerp(endVertex[2], endVertex[4], to), this.lerp(endVertex[3], endVertex[5], to)]
310 |
311 | let h2 = [this.lerp(e2[0], f2[0], to), this.lerp(e2[1], f2[1], to)]
312 | let j2 = [this.lerp(f2[0], g2[0], to), this.lerp(f2[1], g2[1], to)]
313 | let k2 = [this.lerp(h2[0], j2[0], to), this.lerp(h2[1], j2[1], to)]
314 |
315 | values.start = [e2[0], e2[1], startVertex[2], startVertex[3], startVertex[4], startVertex[5]]
316 | values.end = [j2[0], j2[1], h2[0], h2[1], k2[0], k2[1]]
317 |
318 | return values
319 | }
320 | }
321 |
322 | lerp(a: number, b: number, t: number) {
323 | const s = 1 - t
324 | return a * s + b * t
325 | }
326 |
327 | sumArray(arr: number[]) {
328 | function add(a: number, b: number) {
329 | return a + b
330 | }
331 |
332 | return arr.reduce(add)
333 | }
334 |
335 | isStraight(
336 | startX: number,
337 | startY: number,
338 | ctrl1X: number,
339 | ctrl1Y: number,
340 | ctrl2X: number,
341 | ctrl2Y: number,
342 | endX: number,
343 | endY: number
344 | ) {
345 | return startX === ctrl1X && startY === ctrl1Y && endX === ctrl2X && endY === ctrl2Y
346 | }
347 |
348 | setKeyframes(time: number) {}
349 |
350 | reset(reversed: boolean) {}
351 | }
352 |
353 | export default Path
354 |
--------------------------------------------------------------------------------
/src/objects/Polystar.ts:
--------------------------------------------------------------------------------
1 | import Property from '../property/Property'
2 | import AnimatedProperty from '../property/AnimatedProperty'
3 | import { PathProps } from './Path'
4 |
5 | class Polystar {
6 | private readonly starType: number
7 | private readonly points: Property
8 | private readonly innerRadius: Property
9 | private readonly outerRadius: Property
10 | private readonly position?: Property<[number, number]>
11 | private readonly rotation?: Property
12 | private readonly innerRoundness?: Property
13 | private readonly outerRoundness?: Property
14 |
15 | public readonly closed = true // TODO ??
16 |
17 | constructor(data: PathProps) {
18 | this.starType = data.starType
19 | this.points = data.points.length > 1 ? new AnimatedProperty(data.points) : new Property(data.points)
20 |
21 | this.innerRadius =
22 | data.innerRadius.length > 1 ? new AnimatedProperty(data.innerRadius) : new Property(data.innerRadius)
23 | this.outerRadius =
24 | data.outerRadius.length > 1 ? new AnimatedProperty(data.outerRadius) : new Property(data.outerRadius)
25 |
26 | if (data.position) {
27 | this.position = data.position.length > 1 ? new AnimatedProperty(data.position) : new Property(data.position)
28 | }
29 | if (data.rotation) {
30 | this.rotation = data.rotation.length > 1 ? new AnimatedProperty(data.rotation) : new Property(data.rotation)
31 | }
32 | if (data.innerRoundness) {
33 | this.innerRoundness =
34 | data.innerRoundness.length > 1
35 | ? new AnimatedProperty(data.innerRoundness)
36 | : new Property(data.innerRoundness)
37 | }
38 | if (data.outerRoundness) {
39 | this.outerRoundness =
40 | data.outerRoundness.length > 1
41 | ? new AnimatedProperty(data.outerRoundness)
42 | : new Property(data.outerRoundness)
43 | }
44 | }
45 |
46 | draw(ctx: CanvasRenderingContext2D, time: number) {
47 | //todo add trim
48 |
49 | const points = this.points.getValue(time)
50 | const innerRadius = this.innerRadius.getValue(time)
51 | const outerRadius = this.outerRadius.getValue(time)
52 | const position = this.position ? this.position.getValue(time) : [0, 0]
53 | let rotation = this.rotation ? this.rotation.getValue(time) : 0
54 | const innerRoundness = this.innerRoundness ? this.innerRoundness.getValue(time) : 0
55 | const outerRoundness = this.outerRoundness ? this.outerRoundness.getValue(time) : 0
56 |
57 | rotation = this.deg2rad(rotation)
58 | const start = this.rotatePoint(0, 0, 0, 0 - outerRadius, rotation)
59 |
60 | ctx.save()
61 | ctx.beginPath()
62 | ctx.translate(position[0], position[1])
63 | ctx.moveTo(start[0], start[1])
64 |
65 | for (let i = 0; i < points; i++) {
66 | let pInner
67 | let pOuter
68 | let pOuter1Tangent
69 | let pOuter2Tangent
70 | let pInner1Tangent
71 | let pInner2Tangent
72 | let outerOffset
73 | let innerOffset
74 | let rot
75 |
76 | rot = (Math.PI / points) * 2
77 |
78 | pInner = this.rotatePoint(0, 0, 0, 0 - innerRadius, rot * (i + 1) - rot / 2 + rotation)
79 | pOuter = this.rotatePoint(0, 0, 0, 0 - outerRadius, rot * (i + 1) + rotation)
80 |
81 | //FIxME
82 | if (!outerOffset) outerOffset = (((start[0] + pInner[0]) * outerRoundness) / 100) * 0.5522848
83 | if (!innerOffset) innerOffset = (((start[0] + pInner[0]) * innerRoundness) / 100) * 0.5522848
84 |
85 | pOuter1Tangent = this.rotatePoint(0, 0, outerOffset, 0 - outerRadius, rot * i + rotation)
86 | pInner1Tangent = this.rotatePoint(
87 | 0,
88 | 0,
89 | innerOffset * -1,
90 | 0 - innerRadius,
91 | rot * (i + 1) - rot / 2 + rotation
92 | )
93 | pInner2Tangent = this.rotatePoint(0, 0, innerOffset, 0 - innerRadius, rot * (i + 1) - rot / 2 + rotation)
94 | pOuter2Tangent = this.rotatePoint(0, 0, outerOffset * -1, 0 - outerRadius, rot * (i + 1) + rotation)
95 |
96 | if (this.starType === 1) {
97 | //star
98 | ctx.bezierCurveTo(
99 | pOuter1Tangent[0],
100 | pOuter1Tangent[1],
101 | pInner1Tangent[0],
102 | pInner1Tangent[1],
103 | pInner[0],
104 | pInner[1]
105 | )
106 | ctx.bezierCurveTo(
107 | pInner2Tangent[0],
108 | pInner2Tangent[1],
109 | pOuter2Tangent[0],
110 | pOuter2Tangent[1],
111 | pOuter[0],
112 | pOuter[1]
113 | )
114 | } else {
115 | //polygon
116 | ctx.bezierCurveTo(
117 | pOuter1Tangent[0],
118 | pOuter1Tangent[1],
119 | pOuter2Tangent[0],
120 | pOuter2Tangent[1],
121 | pOuter[0],
122 | pOuter[1]
123 | )
124 | }
125 |
126 | //debug
127 | //ctx.fillStyle = "black";
128 | //ctx.fillRect(pInner[0], pInner[1], 5, 5);
129 | //ctx.fillRect(pOuter[0], pOuter[1], 5, 5);
130 | //ctx.fillStyle = "blue";
131 | //ctx.fillRect(pOuter1Tangent[0], pOuter1Tangent[1], 5, 5);
132 | //ctx.fillStyle = "red";
133 | //ctx.fillRect(pInner1Tangent[0], pInner1Tangent[1], 5, 5);
134 | //ctx.fillStyle = "green";
135 | //ctx.fillRect(pInner2Tangent[0], pInner2Tangent[1], 5, 5);
136 | //ctx.fillStyle = "brown";
137 | //ctx.fillRect(pOuter2Tangent[0], pOuter2Tangent[1], 5, 5);
138 | }
139 |
140 | ctx.restore()
141 | }
142 |
143 | rotatePoint(cx: number, cy: number, x: number, y: number, radians: number) {
144 | const cos = Math.cos(radians)
145 | const sin = Math.sin(radians)
146 | const nx = cos * (x - cx) - sin * (y - cy) + cx
147 | const ny = sin * (x - cx) + cos * (y - cy) + cy
148 | return [nx, ny]
149 | }
150 |
151 | deg2rad(deg: number) {
152 | return deg * (Math.PI / 180)
153 | }
154 |
155 | setKeyframes(time: number) {
156 | this.points.setKeyframes(time)
157 | this.innerRadius.setKeyframes(time)
158 | this.outerRadius.setKeyframes(time)
159 | if (this.position) this.position.setKeyframes(time)
160 | if (this.rotation) this.rotation.setKeyframes(time)
161 | if (this.innerRoundness) this.innerRoundness.setKeyframes(time)
162 | if (this.outerRoundness) this.outerRoundness.setKeyframes(time)
163 | }
164 |
165 | reset(reversed: boolean) {
166 | this.points.reset(reversed)
167 | this.innerRadius.reset(reversed)
168 | this.outerRadius.reset(reversed)
169 | if (this.position) this.position.reset(reversed)
170 | if (this.rotation) this.rotation.reset(reversed)
171 | if (this.innerRoundness) this.innerRoundness.reset(reversed)
172 | if (this.outerRoundness) this.outerRoundness.reset(reversed)
173 | }
174 | }
175 |
176 | export default Polystar
177 |
--------------------------------------------------------------------------------
/src/objects/Rect.ts:
--------------------------------------------------------------------------------
1 | import Property from '../property/Property'
2 | import AnimatedProperty from '../property/AnimatedProperty'
3 | import { TrimValues } from '../property/Trim'
4 |
5 | type Options = {
6 | size: []
7 | roundness?: []
8 | position?: []
9 | }
10 |
11 | class Rect {
12 | private readonly size: Property<[number, number]>
13 | private readonly position?: Property<[number, number]>
14 | private readonly roundness?: Property
15 |
16 | public readonly closed = true
17 |
18 | constructor(data: Options) {
19 | this.size = data.size.length > 1 ? new AnimatedProperty(data.size) : new Property(data.size)
20 |
21 | if (data.position) {
22 | this.position = data.position.length > 1 ? new AnimatedProperty(data.position) : new Property(data.position)
23 | }
24 |
25 | if (data.roundness) {
26 | this.roundness =
27 | data.roundness.length > 1 ? new AnimatedProperty(data.roundness) : new Property(data.roundness)
28 | }
29 | }
30 |
31 | draw(ctx: CanvasRenderingContext2D, time: number, trim: TrimValues) {
32 | const size = this.size.getValue(time)
33 | const position = this.position ? this.position.getValue(time) : [0, 0]
34 | let roundness = this.roundness ? this.roundness.getValue(time) : 0
35 |
36 | if (size[0] < 2 * roundness) roundness = size[0] / 2
37 | if (size[1] < 2 * roundness) roundness = size[1] / 2
38 |
39 | const x = position[0] - size[0] / 2
40 | const y = position[1] - size[1] / 2
41 |
42 | if (trim) {
43 | // let tv;
44 | // trim = this.getTrimValues(trim);
45 | //TODO add trim
46 | } else {
47 | ctx.moveTo(x + roundness, y)
48 | ctx.arcTo(x + size[0], y, x + size[0], y + size[1], roundness)
49 | ctx.arcTo(x + size[0], y + size[1], x, y + size[1], roundness)
50 | ctx.arcTo(x, y + size[1], x, y, roundness)
51 | ctx.arcTo(x, y, x + size[0], y, roundness)
52 | }
53 | }
54 |
55 | setKeyframes(time: number) {
56 | this.size.setKeyframes(time)
57 | if (this.position) this.position.setKeyframes(time)
58 | if (this.roundness) this.roundness.setKeyframes(time)
59 | }
60 |
61 | reset(reversed: boolean) {
62 | this.size.reset(reversed)
63 | if (this.position) this.position.reset(reversed)
64 | if (this.roundness) this.roundness.reset(reversed)
65 | }
66 | }
67 |
68 | export default Rect
69 |
--------------------------------------------------------------------------------
/src/property/AnimatedProperty.ts:
--------------------------------------------------------------------------------
1 | import Property, { Frame } from './Property'
2 | import BezierEasing from '../utils/BezierEasing'
3 |
4 | class AnimatedProperty extends Property {
5 | public frameCount: number
6 | public nextFrame: Frame
7 | public lastFrame: Frame
8 | public easing?: any | null
9 | public finished = false
10 | public started = false
11 | public pointer = 0
12 |
13 | constructor(data: Frame[]) {
14 | super(data)
15 | this.frameCount = this.frames.length
16 |
17 | //todo check
18 | this.nextFrame = this.frames[1]
19 | this.lastFrame = this.frames[0]
20 | }
21 |
22 | lerp(a: T, b: T, t: number): T {
23 | if (Array.isArray(a) && Array.isArray(b)) {
24 | const arr = []
25 | for (let i = 0; i < a.length; i++) {
26 | arr[i] = a[i] + t * (b[i] - a[i])
27 | }
28 | return arr as T
29 | } else if (typeof a === 'number' && typeof b === 'number') {
30 | return (a + t * (b - a)) as T
31 | } else {
32 | return a
33 | }
34 | }
35 |
36 | setEasing() {
37 | if (this.nextFrame?.easeIn && this.lastFrame?.easeOut) {
38 | this.easing = new (BezierEasing as any)(
39 | this.lastFrame.easeOut[0],
40 | this.lastFrame.easeOut[1],
41 | this.nextFrame.easeIn[0],
42 | this.nextFrame.easeIn[1]
43 | )
44 | } else {
45 | this.easing = null
46 | }
47 | }
48 |
49 | getValue(time: number): T {
50 | if (this.finished && time >= this.nextFrame.t) {
51 | return this.nextFrame.v
52 | } else if (!this.started && time <= this.lastFrame.t) {
53 | return this.lastFrame.v
54 | } else {
55 | this.started = true
56 | this.finished = false
57 | if (time > this.nextFrame.t) {
58 | if (this.pointer + 1 === this.frameCount) {
59 | this.finished = true
60 | } else {
61 | this.pointer++
62 | this.lastFrame = this.frames[this.pointer - 1]
63 | this.nextFrame = this.frames[this.pointer]
64 | this.onKeyframeChange()
65 | }
66 | } else if (time < this.lastFrame.t) {
67 | if (this.pointer < 2) {
68 | this.started = false
69 | } else {
70 | this.pointer--
71 | this.lastFrame = this.frames[this.pointer - 1]
72 | this.nextFrame = this.frames[this.pointer]
73 | this.onKeyframeChange()
74 | }
75 | }
76 | return this.getValueAtTime(time)
77 | }
78 | }
79 |
80 | setKeyframes(time: number) {
81 | //console.log(time, this.frames[this.frameCount - 2].t, this.frames[this.frameCount - 1].t);
82 |
83 | if (time < this.frames[0].t) {
84 | this.pointer = 1
85 | this.nextFrame = this.frames[this.pointer]
86 | this.lastFrame = this.frames[this.pointer - 1]
87 | this.onKeyframeChange()
88 | return
89 | }
90 |
91 | if (time > this.frames[this.frameCount - 1].t) {
92 | this.pointer = this.frameCount - 1
93 | this.nextFrame = this.frames[this.pointer]
94 | this.lastFrame = this.frames[this.pointer - 1]
95 | this.onKeyframeChange()
96 | return
97 | }
98 |
99 | for (let i = 1; i < this.frameCount; i++) {
100 | if (time >= this.frames[i - 1].t && time <= this.frames[i].t) {
101 | this.pointer = i
102 | this.lastFrame = this.frames[i - 1]
103 | this.nextFrame = this.frames[i]
104 | this.onKeyframeChange()
105 | return
106 | }
107 | }
108 | }
109 |
110 | onKeyframeChange() {
111 | this.setEasing()
112 | }
113 |
114 | getElapsed(time: number) {
115 | const delta = time - this.lastFrame.t
116 | const duration = this.nextFrame.t - this.lastFrame.t
117 | let elapsed = delta / duration
118 |
119 | if (elapsed > 1) elapsed = 1
120 | else if (elapsed < 0) elapsed = 0
121 | else if (this.easing) elapsed = this.easing(elapsed)
122 | return elapsed
123 | }
124 |
125 | getValueAtTime(time: number): T {
126 | return this.lerp(this.lastFrame.v, this.nextFrame.v, this.getElapsed(time))
127 | }
128 |
129 | reset(reversed: boolean) {
130 | this.finished = false
131 | this.started = false
132 | this.pointer = reversed ? this.frameCount - 1 : 1
133 | this.nextFrame = this.frames[this.pointer]
134 | this.lastFrame = this.frames[this.pointer - 1]
135 | this.onKeyframeChange()
136 | }
137 | }
138 |
139 | export default AnimatedProperty
140 |
--------------------------------------------------------------------------------
/src/property/BlendingMode.ts:
--------------------------------------------------------------------------------
1 | class BlendingMode {
2 | private type: GlobalCompositeOperation
3 |
4 | constructor(type: string) {
5 | this.type = type.toLowerCase().replace('_', '-') as GlobalCompositeOperation
6 | }
7 |
8 | setCompositeOperation(ctx: CanvasRenderingContext2D) {
9 | ctx.globalCompositeOperation = this.type
10 | }
11 | }
12 |
13 | export default BlendingMode
14 |
--------------------------------------------------------------------------------
/src/property/Fill.ts:
--------------------------------------------------------------------------------
1 | import Property from './Property'
2 | import AnimatedProperty from './AnimatedProperty'
3 |
4 | export type FillProps = {
5 | color: []
6 | opacity: []
7 | }
8 |
9 | class Fill {
10 | private readonly color: Property<[number, number, number]>
11 | private readonly opacity?: Property
12 |
13 | constructor(data: FillProps) {
14 | this.color = data.color.length > 1 ? new AnimatedProperty(data.color) : new Property(data.color)
15 | if (data.opacity)
16 | this.opacity = data.opacity.length > 1 ? new AnimatedProperty(data.opacity) : new Property(data.opacity)
17 | }
18 |
19 | getValue(time: number) {
20 | const color = this.color.getValue(time)
21 | const opacity = this.opacity ? this.opacity.getValue(time) : 1
22 | return `rgba(${Math.round(color[0])}, ${Math.round(color[1])}, ${Math.round(color[2])}, ${opacity})`
23 | }
24 |
25 | update(ctx: CanvasRenderingContext2D, time: number) {
26 | ctx.fillStyle = this.getValue(time)
27 | }
28 |
29 | setKeyframes(time: number) {
30 | this.color.setKeyframes(time)
31 | if (this.opacity) this.opacity.setKeyframes(time)
32 | }
33 |
34 | reset(reversed: boolean) {
35 | this.color.reset(reversed)
36 | if (this.opacity) this.opacity.reset(reversed)
37 | }
38 | }
39 |
40 | export default Fill
41 |
--------------------------------------------------------------------------------
/src/property/GradientFill.ts:
--------------------------------------------------------------------------------
1 | import Property from './Property'
2 | import AnimatedProperty from './AnimatedProperty'
3 |
4 | type Stop = {
5 | location: number
6 | color: [number, number, number, number]
7 | }
8 |
9 | export type GradientFillProps = {
10 | opacity: []
11 | name: string
12 | stops: Stop[]
13 | type: string
14 | startPoint: []
15 | endPoint: []
16 | }
17 |
18 | class GradientFill {
19 | private readonly type: string
20 | private readonly startPoint: Property<[number, number]>
21 | private readonly endPoint: Property<[number, number]>
22 | private readonly opacity?: Property
23 |
24 | public stops: Stop[]
25 |
26 | constructor(data: GradientFillProps, gradients: { [key: string]: GradientFill[] }) {
27 | if (!gradients[data.name]) gradients[data.name] = []
28 | gradients[data.name].push(this)
29 |
30 | this.stops = data.stops
31 | this.type = data.type
32 | this.startPoint =
33 | data.startPoint.length > 1 ? new AnimatedProperty(data.startPoint) : new Property(data.startPoint)
34 | this.endPoint = data.endPoint.length > 1 ? new AnimatedProperty(data.endPoint) : new Property(data.endPoint)
35 | if (data.opacity) {
36 | this.opacity = data.opacity.length > 1 ? new AnimatedProperty(data.opacity) : new Property(data.opacity)
37 | }
38 | }
39 |
40 | update(ctx: CanvasRenderingContext2D, time: number) {
41 | const startPoint = this.startPoint.getValue(time)
42 | const endPoint = this.endPoint.getValue(time)
43 | let radius = 0
44 |
45 | if (this.type === 'radial') {
46 | const distX = startPoint[0] - endPoint[0]
47 | const distY = startPoint[1] - endPoint[1]
48 | radius = Math.sqrt(distX * distX + distY * distY)
49 | }
50 |
51 | const gradient =
52 | this.type === 'radial'
53 | ? ctx.createRadialGradient(startPoint[0], startPoint[1], 0, startPoint[0], startPoint[1], radius)
54 | : ctx.createLinearGradient(startPoint[0], startPoint[1], endPoint[0], endPoint[1])
55 |
56 | const opacity = this.opacity ? this.opacity.getValue(time) : 1
57 |
58 | for (const stop of this.stops) {
59 | const color = stop.color
60 | gradient.addColorStop(stop.location, `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3] * opacity})`)
61 | }
62 |
63 | ctx.fillStyle = gradient
64 | }
65 |
66 | setKeyframes(time: number) {
67 | if (this.opacity) this.opacity.setKeyframes(time)
68 | this.startPoint.setKeyframes(time)
69 | this.endPoint.setKeyframes(time)
70 | }
71 |
72 | reset(reversed: boolean) {
73 | if (this.opacity) this.opacity.reset(reversed)
74 | this.startPoint.reset(reversed)
75 | this.endPoint.reset(reversed)
76 | }
77 | }
78 |
79 | export default GradientFill
80 |
--------------------------------------------------------------------------------
/src/property/Property.ts:
--------------------------------------------------------------------------------
1 | export interface Frame {
2 | easeIn?: [number, number]
3 | easeOut?: [number, number]
4 | t: number
5 | v: T
6 | len: number[]
7 | motionpath?: [number, number, number, number, number, number, number, number]
8 | }
9 |
10 | class Property {
11 | public frames: Frame[]
12 |
13 | constructor(data: Frame[]) {
14 | this.frames = data
15 | }
16 |
17 | getValue(time: number): T {
18 | return this.frames[0].v
19 | }
20 |
21 | setKeyframes(time: number) {}
22 |
23 | reset(reversed: boolean) {}
24 | }
25 |
26 | export default Property
27 |
--------------------------------------------------------------------------------
/src/property/Stroke.ts:
--------------------------------------------------------------------------------
1 | import Property from './Property'
2 | import AnimatedProperty from './AnimatedProperty'
3 |
4 | export type StrokeProps = {
5 | join: CanvasLineJoin
6 | cap: CanvasLineCap
7 | miterLimit: []
8 | color: []
9 | opacity: []
10 | width: []
11 | dashes: { dash: []; gap: []; offset: [] }
12 | }
13 |
14 | class Stroke {
15 | private readonly join: CanvasLineJoin
16 | private readonly cap: CanvasLineCap
17 | private readonly miterLimit?: Property
18 | private readonly color: Property<[number, number, number]>
19 | private readonly opacity: Property
20 | private readonly width: Property
21 | private readonly dashes?: { dash: Property; gap: Property; offset: Property }
22 |
23 | constructor(data: StrokeProps) {
24 | this.join = data.join
25 | this.cap = data.cap
26 |
27 | if (data.miterLimit) {
28 | if (data.miterLimit.length > 1) this.miterLimit = new AnimatedProperty(data.miterLimit)
29 | else this.miterLimit = new Property(data.miterLimit)
30 | }
31 |
32 | if (data.color.length > 1) this.color = new AnimatedProperty(data.color)
33 | else this.color = new Property(data.color)
34 |
35 | if (data.opacity.length > 1) this.opacity = new AnimatedProperty(data.opacity)
36 | else this.opacity = new Property(data.opacity)
37 |
38 | if (data.width.length > 1) this.width = new AnimatedProperty(data.width)
39 | else this.width = new Property(data.width)
40 |
41 | if (data.dashes) {
42 | const { dash, gap, offset } = data.dashes
43 |
44 | this.dashes = {
45 | dash: dash.length > 1 ? new AnimatedProperty(dash) : new Property(dash),
46 | gap: gap.length > 1 ? new AnimatedProperty(gap) : new Property(gap),
47 | offset: offset.length > 1 ? new AnimatedProperty(offset) : new Property(offset),
48 | }
49 | }
50 | }
51 |
52 | getValue(time: number) {
53 | const color = this.color.getValue(time)
54 | const opacity = this.opacity.getValue(time)
55 | color[0] = Math.round(color[0])
56 | color[1] = Math.round(color[1])
57 | color[2] = Math.round(color[2])
58 | const s = color.join(', ')
59 |
60 | return `rgba(${s}, ${opacity})`
61 | }
62 |
63 | update(ctx: CanvasRenderingContext2D, time: number) {
64 | const strokeColor = this.getValue(time)
65 | const strokeWidth = this.width.getValue(time)
66 | const strokeJoin = this.join
67 | let miterLimit
68 | if (strokeJoin === 'miter' && this.miterLimit) miterLimit = this.miterLimit.getValue(time)
69 |
70 | ctx.lineWidth = strokeWidth
71 | ctx.lineJoin = strokeJoin
72 | if (miterLimit) ctx.miterLimit = miterLimit
73 | ctx.lineCap = this.cap
74 | ctx.strokeStyle = strokeColor
75 |
76 | if (this.dashes) {
77 | ctx.setLineDash([this.dashes?.dash?.getValue(time), this.dashes?.gap?.getValue(time)])
78 | ctx.lineDashOffset = this.dashes?.offset?.getValue(time)
79 | }
80 | }
81 |
82 | setKeyframes(time: number) {
83 | this.color.setKeyframes(time)
84 | this.opacity.setKeyframes(time)
85 | this.width.setKeyframes(time)
86 | if (this.miterLimit) this.miterLimit.setKeyframes(time)
87 | if (this.dashes) {
88 | this.dashes?.dash?.setKeyframes(time)
89 | this.dashes?.gap?.setKeyframes(time)
90 | this.dashes?.offset?.setKeyframes(time)
91 | }
92 | }
93 |
94 | reset(reversed: boolean) {
95 | this.color.reset(reversed)
96 | this.opacity.reset(reversed)
97 | this.width.reset(reversed)
98 | if (this.miterLimit) this.miterLimit.reset(reversed)
99 | if (this.dashes) {
100 | this.dashes?.dash?.reset(reversed)
101 | this.dashes?.gap?.reset(reversed)
102 | this.dashes?.offset?.reset(reversed)
103 | }
104 | }
105 | }
106 |
107 | export default Stroke
108 |
--------------------------------------------------------------------------------
/src/property/Trim.ts:
--------------------------------------------------------------------------------
1 | import Property from './Property'
2 | import AnimatedProperty from './AnimatedProperty'
3 |
4 | export type TrimProps = {
5 | type: string
6 | start: []
7 | end: []
8 | offset: []
9 | }
10 |
11 | export type TrimValues = { start: number; end: number } | null
12 |
13 | class Trim {
14 | private type: string
15 | private readonly start?: Property
16 | private readonly end?: Property
17 | private readonly offset?: Property
18 |
19 | constructor(data: TrimProps) {
20 | this.type = data.type
21 |
22 | if (data.start) this.start = data.start.length > 1 ? new AnimatedProperty(data.start) : new Property(data.start)
23 | if (data.end) this.end = data.end.length > 1 ? new AnimatedProperty(data.end) : new Property(data.end)
24 | if (data.offset)
25 | this.offset = data.offset.length > 1 ? new AnimatedProperty(data.offset) : new Property(data.offset)
26 | }
27 |
28 | getTrim(time: number): TrimValues {
29 | const startValue = this.start ? this.start.getValue(time) : 0
30 | const endValue = this.end ? this.end.getValue(time) : 1
31 |
32 | let start = Math.min(startValue, endValue)
33 | let end = Math.max(startValue, endValue)
34 |
35 | if (this.offset) {
36 | const offset = this.offset.getValue(time) % 1
37 | if ((offset > 0 && offset < 1) || (offset > -1 && offset < 0)) {
38 | start += offset
39 | end += offset
40 | start = start > 1 ? start - 1 : start
41 | start = start < 0 ? 1 + start : start
42 | end = end > 1 ? end - 1 : end
43 | end = end < 0 ? 1 + end : end
44 | }
45 | }
46 |
47 | if (start === 0 && end === 1) {
48 | return null
49 | } else {
50 | return { start, end }
51 | }
52 | }
53 |
54 | setKeyframes(time: number) {
55 | if (this.start) this.start.setKeyframes(time)
56 | if (this.end) this.end.setKeyframes(time)
57 | if (this.offset) this.offset.setKeyframes(time)
58 | }
59 |
60 | reset(reversed: boolean) {
61 | if (this.start) this.start.reset(reversed)
62 | if (this.end) this.end.reset(reversed)
63 | if (this.offset) this.offset.reset(reversed)
64 | }
65 | }
66 |
67 | export default Trim
68 |
--------------------------------------------------------------------------------
/src/transform/Position.ts:
--------------------------------------------------------------------------------
1 | import Bezier from '../utils/Bezier'
2 | import AnimatedProperty from '../property/AnimatedProperty'
3 |
4 | class Position extends AnimatedProperty<[number, number]> {
5 | private motionpath?: Bezier | null
6 |
7 | onKeyframeChange() {
8 | this.setEasing()
9 | this.setMotionPath()
10 | }
11 |
12 | getValueAtTime(time: number): [number, number] {
13 | if (this.motionpath) {
14 | return this.motionpath.getValues(this.getElapsed(time))
15 | } else {
16 | return this.lerp(this.lastFrame.v, this.nextFrame.v, this.getElapsed(time))
17 | }
18 | }
19 |
20 | setMotionPath() {
21 | if (this.lastFrame.motionpath) {
22 | this.motionpath = new Bezier(this.lastFrame.motionpath)
23 | // @ts-ignore
24 | // TODO fix
25 | this.motionpath.getLength(this.lastFrame.len)
26 | } else {
27 | this.motionpath = null
28 | }
29 | }
30 | }
31 |
32 | export default Position
33 |
--------------------------------------------------------------------------------
/src/transform/Transform.ts:
--------------------------------------------------------------------------------
1 | import Property from '../property/Property'
2 | import AnimatedProperty from '../property/AnimatedProperty'
3 | import Position from './Position'
4 |
5 | export type TransformProps = {
6 | position?: []
7 | positionX?: []
8 | positionY?: []
9 | anchor?: []
10 | scaleX?: []
11 | scaleY?: []
12 | skew?: []
13 | skewAxis?: []
14 | rotation?: []
15 | opacity?: []
16 | }
17 |
18 | class Transform {
19 | private readonly position?: Property<[number, number]>
20 | private readonly positionX?: Property
21 | private readonly positionY?: Property
22 | private readonly anchor?: Property<[number, number]>
23 | private readonly scaleX?: Property
24 | private readonly scaleY?: Property
25 | private readonly skew?: Property
26 | private readonly skewAxis?: Property
27 | private readonly rotation?: Property
28 | private readonly opacity?: Property
29 |
30 | constructor(data: TransformProps) {
31 | if (data.position) {
32 | if (data.position.length > 1) {
33 | this.position = new Position(data.position)
34 | } else {
35 | this.position = new Property(data.position)
36 | }
37 | }
38 |
39 | if (data.positionX) {
40 | this.positionX =
41 | data.positionX.length > 1 ? new AnimatedProperty(data.positionX) : new Property(data.positionX)
42 | }
43 | if (data.positionY) {
44 | this.positionY =
45 | data.positionY.length > 1 ? new AnimatedProperty(data.positionY) : new Property(data.positionY)
46 | }
47 | if (data.anchor) {
48 | this.anchor = data.anchor.length > 1 ? new AnimatedProperty(data.anchor) : new Property(data.anchor)
49 | }
50 | if (data.scaleX) {
51 | this.scaleX = data.scaleX.length > 1 ? new AnimatedProperty(data.scaleX) : new Property(data.scaleX)
52 | }
53 | if (data.scaleY) {
54 | this.scaleY = data.scaleY.length > 1 ? new AnimatedProperty(data.scaleY) : new Property(data.scaleY)
55 | }
56 | if (data.skew) {
57 | this.skew = data.skew.length > 1 ? new AnimatedProperty(data.skew) : new Property(data.skew)
58 | }
59 | if (data.skewAxis) {
60 | this.skewAxis = data.skewAxis.length > 1 ? new AnimatedProperty(data.skewAxis) : new Property(data.skewAxis)
61 | }
62 | if (data.rotation) {
63 | this.rotation = data.rotation.length > 1 ? new AnimatedProperty(data.rotation) : new Property(data.rotation)
64 | }
65 | if (data.opacity) {
66 | this.opacity = data.opacity.length > 1 ? new AnimatedProperty(data.opacity) : new Property(data.opacity)
67 | }
68 | }
69 |
70 | update(ctx: CanvasRenderingContext2D, time: number, setOpacity = true) {
71 | // FIXME wrong transparency if nested
72 | let positionX
73 | let positionY
74 | let opacity = 1
75 |
76 | const anchor = this.anchor ? this.anchor.getValue(time) : [0, 0]
77 | const rotation = this.rotation ? this.deg2rad(this.rotation.getValue(time)) : 0
78 | const skew = this.skew ? this.deg2rad(this.skew.getValue(time)) : 0
79 | const skewAxis = this.skewAxis ? this.deg2rad(this.skewAxis.getValue(time)) : 0
80 | const scaleX = this.scaleX ? this.scaleX.getValue(time) : 1
81 | const scaleY = this.scaleY ? this.scaleY.getValue(time) : 1
82 |
83 | if (setOpacity) {
84 | opacity = this.opacity ? this.opacity.getValue(time) * ctx.globalAlpha : ctx.globalAlpha
85 | }
86 |
87 | if (this.position) {
88 | const position = this.position.getValue(time)
89 | positionX = position[0]
90 | positionY = position[1]
91 | } else {
92 | positionX = this.positionX ? this.positionX.getValue(time) : 0
93 | positionY = this.positionY ? this.positionY.getValue(time) : 0
94 | }
95 |
96 | //order very very important :)
97 | ctx.transform(1, 0, 0, 1, positionX - anchor[0], positionY - anchor[1])
98 | this.setRotation(ctx, rotation, anchor[0], anchor[1])
99 | this.setSkew(ctx, skew, skewAxis, anchor[0], anchor[1])
100 | this.setScale(ctx, scaleX, scaleY, anchor[0], anchor[1])
101 |
102 | if (setOpacity) {
103 | ctx.globalAlpha = opacity
104 | }
105 | }
106 |
107 | setRotation(ctx: CanvasRenderingContext2D, rad: number, x: number, y: number) {
108 | const c = Math.cos(rad)
109 | const s = Math.sin(rad)
110 | const dx = x - c * x + s * y
111 | const dy = y - s * x - c * y
112 | ctx.transform(c, s, -s, c, dx, dy)
113 | }
114 |
115 | setScale(ctx: CanvasRenderingContext2D, sx: number, sy: number, x: number, y: number) {
116 | ctx.transform(sx, 0, 0, sy, -x * sx + x, -y * sy + y)
117 | }
118 |
119 | setSkew(ctx: CanvasRenderingContext2D, skew: number, axis: number, x: number, y: number) {
120 | const t = Math.tan(-skew)
121 | this.setRotation(ctx, -axis, x, y)
122 | ctx.transform(1, 0, t, 1, -y * t, 0)
123 | this.setRotation(ctx, axis, x, y)
124 | }
125 |
126 | deg2rad(deg: number) {
127 | return deg * (Math.PI / 180)
128 | }
129 |
130 | setKeyframes(time: number) {
131 | if (this.anchor) this.anchor.setKeyframes(time)
132 | if (this.rotation) this.rotation.setKeyframes(time)
133 | if (this.skew) this.skew.setKeyframes(time)
134 | if (this.skewAxis) this.skewAxis.setKeyframes(time)
135 | if (this.position) this.position.setKeyframes(time)
136 | if (this.positionX) this.positionX.setKeyframes(time)
137 | if (this.positionY) this.positionY.setKeyframes(time)
138 | if (this.scaleX) this.scaleX.setKeyframes(time)
139 | if (this.scaleY) this.scaleY.setKeyframes(time)
140 | if (this.opacity) this.opacity.setKeyframes(time)
141 | }
142 |
143 | reset(reversed: boolean) {
144 | if (this.anchor) this.anchor.reset(reversed)
145 | if (this.rotation) this.rotation.reset(reversed)
146 | if (this.skew) this.skew.reset(reversed)
147 | if (this.skewAxis) this.skewAxis.reset(reversed)
148 | if (this.position) this.position.reset(reversed)
149 | if (this.positionX) this.positionX.reset(reversed)
150 | if (this.positionY) this.positionY.reset(reversed)
151 | if (this.scaleX) this.scaleX.reset(reversed)
152 | if (this.scaleY) this.scaleY.reset(reversed)
153 | if (this.opacity) this.opacity.reset(reversed)
154 | }
155 | }
156 |
157 | export default Transform
158 |
--------------------------------------------------------------------------------
/src/utils/Bezier.ts:
--------------------------------------------------------------------------------
1 | type Path = [number, number, number, number, number, number, number, number]
2 |
3 | class Bezier {
4 | private readonly path: Path
5 | private steps = 0
6 | private length = 0
7 | private arcLengths: number[] = []
8 |
9 | constructor(path: Path) {
10 | this.path = path
11 | }
12 |
13 | getLength(len: number) {
14 | this.steps = Math.max(Math.floor(len / 10), 1)
15 | this.arcLengths = new Array(this.steps + 1)
16 | this.arcLengths[0] = 0
17 |
18 | let ox = this.cubicN(0, this.path[0], this.path[2], this.path[4], this.path[6])
19 | let oy = this.cubicN(0, this.path[1], this.path[3], this.path[5], this.path[7])
20 | let clen = 0
21 | const iterator = 1 / this.steps
22 |
23 | for (let i = 1; i <= this.steps; i += 1) {
24 | const x = this.cubicN(i * iterator, this.path[0], this.path[2], this.path[4], this.path[6])
25 | const y = this.cubicN(i * iterator, this.path[1], this.path[3], this.path[5], this.path[7])
26 | const dx = ox - x
27 | const dy = oy - y
28 |
29 | clen += Math.sqrt(dx * dx + dy * dy)
30 | this.arcLengths[i] = clen
31 |
32 | ox = x
33 | oy = y
34 | }
35 |
36 | this.length = clen
37 | }
38 |
39 | map(u: number) {
40 | const targetLength = u * this.arcLengths[this.steps]
41 | let low = 0
42 | let high = this.steps
43 | let index = 0
44 |
45 | while (low < high) {
46 | index = low + (((high - low) / 2) | 0)
47 | if (this.arcLengths[index] < targetLength) {
48 | low = index + 1
49 | } else {
50 | high = index
51 | }
52 | }
53 | if (this.arcLengths[index] > targetLength) {
54 | index--
55 | }
56 |
57 | const lengthBefore = this.arcLengths[index]
58 | if (lengthBefore === targetLength) {
59 | return index / this.steps
60 | } else {
61 | return (index + (targetLength - lengthBefore) / (this.arcLengths[index + 1] - lengthBefore)) / this.steps
62 | }
63 | }
64 |
65 | getValues(elapsed: number): [number, number] {
66 | const t = this.map(elapsed)
67 | const x = this.cubicN(t, this.path[0], this.path[2], this.path[4], this.path[6])
68 | const y = this.cubicN(t, this.path[1], this.path[3], this.path[5], this.path[7])
69 |
70 | return [x, y]
71 | }
72 |
73 | cubicN(pct: number, a: number, b: number, c: number, d: number) {
74 | const t2 = pct * pct
75 | const t3 = t2 * pct
76 | return (
77 | a +
78 | (-a * 3 + pct * (3 * a - a * pct)) * pct +
79 | (3 * b + pct * (-6 * b + b * 3 * pct)) * pct +
80 | (c * 3 - c * 3 * pct) * t2 +
81 | d * t3
82 | )
83 | }
84 | }
85 |
86 | export default Bezier
87 |
--------------------------------------------------------------------------------
/src/utils/BezierEasing.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * BezierEasing - use bezier curve for transition easing function
3 | * is based on Firefox's nsSMILKeySpline.cpp
4 | * Usage:
5 | * var spline = BezierEasing(0.25, 0.1, 0.25, 1.0)
6 | * spline(x) => returns the easing value | x must be in [0, 1] range
7 | *
8 | */
9 |
10 | // These values are established by empiricism with tests (tradeoff: performance VS precision)
11 | var NEWTON_ITERATIONS = 4
12 | var NEWTON_MIN_SLOPE = 0.001
13 | var SUBDIVISION_PRECISION = 0.0000001
14 | var SUBDIVISION_MAX_ITERATIONS = 10
15 |
16 | var kSplineTableSize = 11
17 | var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0)
18 |
19 | var float32ArraySupported = typeof Float32Array === 'function'
20 |
21 | function BezierEasing(mX1: number, mY1: number, mX2: number, mY2: number) {
22 | // Validate arguments
23 | if (arguments.length !== 4) {
24 | throw new Error('BezierEasing requires 4 arguments.')
25 | }
26 | for (var i = 0; i < 4; ++i) {
27 | if (typeof arguments[i] !== 'number' || isNaN(arguments[i]) || !isFinite(arguments[i])) {
28 | throw new Error('BezierEasing arguments should be integers.')
29 | }
30 | }
31 | if (mX1 < 0 || mX1 > 1 || mX2 < 0 || mX2 > 1) {
32 | throw new Error('BezierEasing x values must be in [0, 1] range.')
33 | }
34 |
35 | var mSampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize)
36 |
37 | function A(aA1: number, aA2: number) {
38 | return 1.0 - 3.0 * aA2 + 3.0 * aA1
39 | }
40 | function B(aA1: number, aA2: number) {
41 | return 3.0 * aA2 - 6.0 * aA1
42 | }
43 | function C(aA1: number) {
44 | return 3.0 * aA1
45 | }
46 |
47 | // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
48 | function calcBezier(aT: number, aA1: number, aA2: number) {
49 | return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT
50 | }
51 |
52 | // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
53 | function getSlope(aT: number, aA1: number, aA2: number) {
54 | return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1)
55 | }
56 |
57 | function newtonRaphsonIterate(aX: number, aGuessT: number) {
58 | for (var i = 0; i < NEWTON_ITERATIONS; ++i) {
59 | var currentSlope = getSlope(aGuessT, mX1, mX2)
60 | if (currentSlope === 0.0) return aGuessT
61 | var currentX = calcBezier(aGuessT, mX1, mX2) - aX
62 | aGuessT -= currentX / currentSlope
63 | }
64 | return aGuessT
65 | }
66 |
67 | function calcSampleValues() {
68 | for (var i = 0; i < kSplineTableSize; ++i) {
69 | mSampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2)
70 | }
71 | }
72 |
73 | function binarySubdivide(aX: number, aA: number, aB: number) {
74 | var currentX,
75 | currentT,
76 | i = 0
77 | do {
78 | currentT = aA + (aB - aA) / 2.0
79 | currentX = calcBezier(currentT, mX1, mX2) - aX
80 | if (currentX > 0.0) {
81 | aB = currentT
82 | } else {
83 | aA = currentT
84 | }
85 | } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS)
86 | return currentT
87 | }
88 |
89 | function getTForX(aX: number) {
90 | var intervalStart = 0.0
91 | var currentSample = 1
92 | var lastSample = kSplineTableSize - 1
93 |
94 | for (; currentSample != lastSample && mSampleValues[currentSample] <= aX; ++currentSample) {
95 | intervalStart += kSampleStepSize
96 | }
97 | --currentSample
98 |
99 | // Interpolate to provide an initial guess for t
100 | var dist =
101 | (aX - mSampleValues[currentSample]) / (mSampleValues[currentSample + 1] - mSampleValues[currentSample])
102 | var guessForT = intervalStart + dist * kSampleStepSize
103 |
104 | var initialSlope = getSlope(guessForT, mX1, mX2)
105 | if (initialSlope >= NEWTON_MIN_SLOPE) {
106 | return newtonRaphsonIterate(aX, guessForT)
107 | } else if (initialSlope == 0.0) {
108 | return guessForT
109 | } else {
110 | return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize)
111 | }
112 | }
113 |
114 | if (mX1 != mY1 || mX2 != mY2) calcSampleValues()
115 |
116 | var f = function (aX: number) {
117 | if (mX1 === mY1 && mX2 === mY2) return aX // linear
118 | // Because JavaScript number are imprecise, we should guarantee the extremes are right.
119 | if (aX === 0) return 0
120 | if (aX === 1) return 1
121 | return calcBezier(getTForX(aX), mY1, mY2)
122 | }
123 | var str = 'BezierEasing(' + [mX1, mY1, mX2, mY2] + ')'
124 | f.toString = function () {
125 | return str
126 | }
127 |
128 | return f
129 | }
130 |
131 | // CSS mapping
132 | BezierEasing.css = {
133 | ease: BezierEasing(0.25, 0.1, 0.25, 1.0),
134 | linear: BezierEasing(0.0, 0.0, 1.0, 1.0),
135 | 'ease-in': BezierEasing(0.42, 0.0, 1.0, 1.0),
136 | 'ease-out': BezierEasing(0.0, 0.0, 0.58, 1.0),
137 | 'ease-in-out': BezierEasing(0.42, 0.0, 0.58, 1.0),
138 | }
139 |
140 | export default BezierEasing
141 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "emitDeclarationOnly": true,
5 | "declaration": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "strictNullChecks": true,
8 | "strict": true,
9 | "noImplicitAny": true,
10 | "esModuleInterop": true,
11 | "lib": ["dom", "esnext"]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@esbuild/android-arm64@0.17.19":
6 | version "0.17.19"
7 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd"
8 | integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==
9 |
10 | "@esbuild/android-arm@0.17.19":
11 | version "0.17.19"
12 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d"
13 | integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==
14 |
15 | "@esbuild/android-x64@0.17.19":
16 | version "0.17.19"
17 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1"
18 | integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==
19 |
20 | "@esbuild/darwin-arm64@0.17.19":
21 | version "0.17.19"
22 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276"
23 | integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==
24 |
25 | "@esbuild/darwin-x64@0.17.19":
26 | version "0.17.19"
27 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb"
28 | integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==
29 |
30 | "@esbuild/freebsd-arm64@0.17.19":
31 | version "0.17.19"
32 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2"
33 | integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==
34 |
35 | "@esbuild/freebsd-x64@0.17.19":
36 | version "0.17.19"
37 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4"
38 | integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==
39 |
40 | "@esbuild/linux-arm64@0.17.19":
41 | version "0.17.19"
42 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb"
43 | integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==
44 |
45 | "@esbuild/linux-arm@0.17.19":
46 | version "0.17.19"
47 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a"
48 | integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==
49 |
50 | "@esbuild/linux-ia32@0.17.19":
51 | version "0.17.19"
52 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a"
53 | integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==
54 |
55 | "@esbuild/linux-loong64@0.17.19":
56 | version "0.17.19"
57 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72"
58 | integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==
59 |
60 | "@esbuild/linux-mips64el@0.17.19":
61 | version "0.17.19"
62 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289"
63 | integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==
64 |
65 | "@esbuild/linux-ppc64@0.17.19":
66 | version "0.17.19"
67 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7"
68 | integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==
69 |
70 | "@esbuild/linux-riscv64@0.17.19":
71 | version "0.17.19"
72 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09"
73 | integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==
74 |
75 | "@esbuild/linux-s390x@0.17.19":
76 | version "0.17.19"
77 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829"
78 | integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==
79 |
80 | "@esbuild/linux-x64@0.17.19":
81 | version "0.17.19"
82 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4"
83 | integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==
84 |
85 | "@esbuild/netbsd-x64@0.17.19":
86 | version "0.17.19"
87 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462"
88 | integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==
89 |
90 | "@esbuild/openbsd-x64@0.17.19":
91 | version "0.17.19"
92 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691"
93 | integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==
94 |
95 | "@esbuild/sunos-x64@0.17.19":
96 | version "0.17.19"
97 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273"
98 | integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==
99 |
100 | "@esbuild/win32-arm64@0.17.19":
101 | version "0.17.19"
102 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f"
103 | integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==
104 |
105 | "@esbuild/win32-ia32@0.17.19":
106 | version "0.17.19"
107 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03"
108 | integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==
109 |
110 | "@esbuild/win32-x64@0.17.19":
111 | version "0.17.19"
112 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061"
113 | integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==
114 |
115 | esbuild@0.17.19:
116 | version "0.17.19"
117 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955"
118 | integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==
119 | optionalDependencies:
120 | "@esbuild/android-arm" "0.17.19"
121 | "@esbuild/android-arm64" "0.17.19"
122 | "@esbuild/android-x64" "0.17.19"
123 | "@esbuild/darwin-arm64" "0.17.19"
124 | "@esbuild/darwin-x64" "0.17.19"
125 | "@esbuild/freebsd-arm64" "0.17.19"
126 | "@esbuild/freebsd-x64" "0.17.19"
127 | "@esbuild/linux-arm" "0.17.19"
128 | "@esbuild/linux-arm64" "0.17.19"
129 | "@esbuild/linux-ia32" "0.17.19"
130 | "@esbuild/linux-loong64" "0.17.19"
131 | "@esbuild/linux-mips64el" "0.17.19"
132 | "@esbuild/linux-ppc64" "0.17.19"
133 | "@esbuild/linux-riscv64" "0.17.19"
134 | "@esbuild/linux-s390x" "0.17.19"
135 | "@esbuild/linux-x64" "0.17.19"
136 | "@esbuild/netbsd-x64" "0.17.19"
137 | "@esbuild/openbsd-x64" "0.17.19"
138 | "@esbuild/sunos-x64" "0.17.19"
139 | "@esbuild/win32-arm64" "0.17.19"
140 | "@esbuild/win32-ia32" "0.17.19"
141 | "@esbuild/win32-x64" "0.17.19"
142 |
143 | fs-extra@11.1.1:
144 | version "11.1.1"
145 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d"
146 | integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==
147 | dependencies:
148 | graceful-fs "^4.2.0"
149 | jsonfile "^6.0.1"
150 | universalify "^2.0.0"
151 |
152 | graceful-fs@^4.1.6, graceful-fs@^4.2.0:
153 | version "4.2.11"
154 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
155 | integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
156 |
157 | inherits@2.0.1:
158 | version "2.0.1"
159 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
160 |
161 | jsonfile@^6.0.1:
162 | version "6.1.0"
163 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
164 | integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
165 | dependencies:
166 | universalify "^2.0.0"
167 | optionalDependencies:
168 | graceful-fs "^4.1.6"
169 |
170 | path@0.12.7:
171 | version "0.12.7"
172 | resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
173 | integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==
174 | dependencies:
175 | process "^0.11.1"
176 | util "^0.10.3"
177 |
178 | prettier@2.8.8:
179 | version "2.8.8"
180 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
181 | integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
182 |
183 | process@^0.11.1:
184 | version "0.11.10"
185 | resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
186 |
187 | tiny-emitter@2.0.2:
188 | version "2.0.2"
189 | resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"
190 | integrity sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==
191 |
192 | typescript@5.0.4:
193 | version "5.0.4"
194 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"
195 | integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
196 |
197 | universalify@^2.0.0:
198 | version "2.0.0"
199 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
200 | integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
201 |
202 | util@^0.10.3:
203 | version "0.10.3"
204 | resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
205 | dependencies:
206 | inherits "2.0.1"
207 |
--------------------------------------------------------------------------------