├── .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 | --------------------------------------------------------------------------------