etro 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.env +2 -0
  2. package/.github/workflows/nodejs.yml +27 -0
  3. package/CHANGELOG.md +109 -0
  4. package/CODE_OF_CONDUCT.md +77 -0
  5. package/CONTRIBUTING.md +155 -0
  6. package/LICENSE +674 -0
  7. package/README.md +57 -0
  8. package/dist/etro.js +3397 -0
  9. package/docs/effect.js.html +1215 -0
  10. package/docs/event.js.html +145 -0
  11. package/docs/index.html +81 -0
  12. package/docs/index.js.html +92 -0
  13. package/docs/layer.js.html +888 -0
  14. package/docs/module-effect-GaussianBlurComponent.html +345 -0
  15. package/docs/module-effect.Brightness.html +339 -0
  16. package/docs/module-effect.Channels.html +319 -0
  17. package/docs/module-effect.ChromaKey.html +611 -0
  18. package/docs/module-effect.Contrast.html +339 -0
  19. package/docs/module-effect.EllipticalMask.html +200 -0
  20. package/docs/module-effect.GaussianBlur.html +202 -0
  21. package/docs/module-effect.GaussianBlurHorizontal.html +242 -0
  22. package/docs/module-effect.GaussianBlurVertical.html +242 -0
  23. package/docs/module-effect.Pixelate.html +330 -0
  24. package/docs/module-effect.Shader.html +1227 -0
  25. package/docs/module-effect.Stack.html +406 -0
  26. package/docs/module-effect.Transform.Matrix.html +193 -0
  27. package/docs/module-effect.Transform.html +1174 -0
  28. package/docs/module-effect.html +148 -0
  29. package/docs/module-event.html +473 -0
  30. package/docs/module-index.html +186 -0
  31. package/docs/module-layer-Media.html +1116 -0
  32. package/docs/module-layer-MediaMixin.html +164 -0
  33. package/docs/module-layer.Audio.html +1188 -0
  34. package/docs/module-layer.Base.html +629 -0
  35. package/docs/module-layer.Image.html +1421 -0
  36. package/docs/module-layer.Text.html +1731 -0
  37. package/docs/module-layer.Video.html +1938 -0
  38. package/docs/module-layer.Visual.html +1698 -0
  39. package/docs/module-layer.html +137 -0
  40. package/docs/module-movie.html +3118 -0
  41. package/docs/module-util.Color.html +702 -0
  42. package/docs/module-util.Font.html +395 -0
  43. package/docs/module-util.html +845 -0
  44. package/docs/movie.js.html +689 -0
  45. package/docs/scripts/collapse.js +20 -0
  46. package/docs/scripts/linenumber.js +25 -0
  47. package/docs/scripts/nav.js +12 -0
  48. package/docs/scripts/polyfill.js +4 -0
  49. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  50. package/docs/scripts/prettify/lang-css.js +2 -0
  51. package/docs/scripts/prettify/prettify.js +28 -0
  52. package/docs/scripts/search.js +83 -0
  53. package/docs/styles/jsdoc.css +671 -0
  54. package/docs/styles/prettify.css +79 -0
  55. package/docs/util.js.html +503 -0
  56. package/eslint.conf.js +28 -0
  57. package/eslint.test-conf.js +4 -0
  58. package/examples/application/readme-screenshot.html +86 -0
  59. package/examples/application/video-player.html +131 -0
  60. package/examples/application/webcam.html +28 -0
  61. package/examples/introduction/audio.html +52 -0
  62. package/examples/introduction/effects.html +56 -0
  63. package/examples/introduction/export.html +70 -0
  64. package/examples/introduction/functions.html +35 -0
  65. package/examples/introduction/hello-world1.html +33 -0
  66. package/examples/introduction/hello-world2.html +32 -0
  67. package/examples/introduction/keyframes.html +67 -0
  68. package/examples/introduction/media.html +55 -0
  69. package/examples/introduction/text.html +27 -0
  70. package/jsdoc.conf.json +3 -0
  71. package/karma.conf.js +60 -0
  72. package/package.json +63 -0
  73. package/private-todo.txt +70 -0
  74. package/rename-file.sh +18 -0
  75. package/rename-versions.sh +14 -0
  76. package/rename.sh +22 -0
  77. package/rollup.config.js +31 -0
  78. package/screenshots/2019-08-17_0.png +0 -0
  79. package/scripts/gen-effect-samples.html +99 -0
  80. package/scripts/save-effect-samples.js +43 -0
  81. package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
  82. package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
  83. package/spec/assets/effect/original.png +0 -0
  84. package/spec/assets/effect/pixelate.png +0 -0
  85. package/spec/assets/effect/transform/multiply.png +0 -0
  86. package/spec/assets/effect/transform/rotate.png +0 -0
  87. package/spec/assets/effect/transform/scale-fraction.png +0 -0
  88. package/spec/assets/effect/transform/scale.png +0 -0
  89. package/spec/assets/effect/transform/translate-fraction.png +0 -0
  90. package/spec/assets/effect/transform/translate.png +0 -0
  91. package/spec/assets/layer/audio.wav +0 -0
  92. package/spec/assets/layer/image.jpg +0 -0
  93. package/spec/effect.spec.js +352 -0
  94. package/spec/event.spec.js +25 -0
  95. package/spec/layer.spec.js +128 -0
  96. package/spec/movie.spec.js +154 -0
  97. package/spec/util.spec.js +285 -0
  98. package/src/effect.js +1265 -0
  99. package/src/event.js +78 -0
  100. package/src/index.js +23 -0
  101. package/src/layer.js +875 -0
  102. package/src/movie.js +636 -0
  103. package/src/util.js +487 -0
package/src/util.js ADDED
@@ -0,0 +1,487 @@
1
+ /**
2
+ * @module util
3
+ */
4
+
5
+ import { publish } from './event.js'
6
+
7
+ /**
8
+ * Merges `options` with `defaultOptions`, and then copies the properties with the keys in `defaultOptions`
9
+ * from the merged object to `destObj`.
10
+ *
11
+ * @return {undefined}
12
+ * @todo Make methods like getDefaultOptions private
13
+ */
14
+ export function applyOptions (options, destObj) {
15
+ const defaultOptions = destObj.getDefaultOptions()
16
+
17
+ // validate; make sure `keys` doesn't have any extraneous items
18
+ for (const option in options) {
19
+ // eslint-disable-next-line no-prototype-builtins
20
+ if (!defaultOptions.hasOwnProperty(option)) {
21
+ throw new Error("Invalid option: '" + option + "'")
22
+ }
23
+ }
24
+
25
+ // merge options and defaultOptions
26
+ options = { ...defaultOptions, ...options }
27
+
28
+ // copy options
29
+ for (const option in options) {
30
+ if (!(option in destObj)) {
31
+ destObj[option] = options[option]
32
+ }
33
+ }
34
+ }
35
+
36
+ // https://stackoverflow.com/a/8024294/3783155
37
+ /**
38
+ * Get all inherited keys
39
+ * @param {object} obj
40
+ * @param {boolean} excludeObjectClass - don't add properties of the <code>Object</code> prototype
41
+ * @private
42
+ */
43
+ function getAllPropertyNames (obj, excludeObjectClass) {
44
+ let props = []
45
+ do {
46
+ props = props.concat(Object.getOwnPropertyNames(obj))
47
+ } while ((obj = Object.getPrototypeOf(obj)) && (excludeObjectClass ? obj.constructor.name !== 'Object' : true))
48
+ return props
49
+ }
50
+
51
+ /**
52
+ * @return {boolean} <code>true</code> if <code>property</code> is a non-array object and all of its own
53
+ * property keys are numbers or <code>"interpolate"</code> or <code>"interpolationKeys"</code>, and
54
+ * <code>false</code> otherwise.
55
+ */
56
+ function isKeyFrames (property) {
57
+ if ((typeof property !== 'object' || property === null) || Array.isArray(property)) {
58
+ return false
59
+ }
60
+ // is reduce slow? I think it is
61
+ // let keys = Object.keys(property); // own propeties
62
+ const keys = getAllPropertyNames(property, true) // includes non-enumerable properties (except that of `Object`)
63
+ for (let i = 0; i < keys.length; i++) {
64
+ const key = keys[i]
65
+ // convert key to number, because object keys are always converted to strings
66
+ if (isNaN(key) && !(key === 'interpolate' || key === 'interpolationKeys')) {
67
+ return false
68
+ }
69
+ }
70
+ // If it's an empty object, don't treat is as keyframe set.
71
+ // https://stackoverflow.com/a/32108184/3783155
72
+ const isEmpty = property.constructor === Object && Object.entries(property).length === 0
73
+ return !isEmpty
74
+ }
75
+
76
+ // must be cleared at the start of each frame
77
+ const valCache = new WeakMap()
78
+ function cacheValue (element, path, value) {
79
+ if (!valCache.has(element.movie)) {
80
+ valCache.set(element.movie, new WeakMap())
81
+ }
82
+ const movieCache = valCache.get(element.movie)
83
+
84
+ if (!movieCache.has(element)) {
85
+ movieCache.set(element, {})
86
+ }
87
+ const elementCache = movieCache.get(element)
88
+
89
+ elementCache[path] = value
90
+ return value
91
+ }
92
+ function hasCachedValue (element, path) {
93
+ return valCache.has(element.movie) &&
94
+ valCache.get(element.movie).has(element) &&
95
+ path in valCache.get(element.movie).get(element)
96
+ }
97
+ function getCachedValue (element, path) {
98
+ return valCache.get(element.movie).get(element)[path]
99
+ }
100
+ export function clearCachedValues (movie) {
101
+ valCache.delete(movie)
102
+ }
103
+
104
+ /**
105
+ * Calculates the value of keyframe set <code>property</code> at <code>time</code> if
106
+ * <code>property</code> is an array, or returns <code>property</code>, assuming that it's a number.
107
+ *
108
+ * @param {(*|module:util.KeyFrames)} property - value or map of time-to-value pairs for keyframes
109
+ * @param {object} element - the object to which the property belongs
110
+ * @param {number} time - time to calculate keyframes for, if necessary
111
+ *
112
+ * Note that only values used in keyframes that numbers or objects (including arrays) are interpolated.
113
+ * All other values are taken sequentially with no interpolation. JavaScript will convert parsed colors,
114
+ * if created correctly, to their string representations when assigned to a CanvasRenderingContext2D property
115
+ * (I'm pretty sure).
116
+ *
117
+ * @todo Is this function efficient?
118
+ * @todo Update doc @params to allow for keyframes
119
+ *
120
+ * @typedef {Object} module:util.KeyFrames
121
+ * @property {function} interpolate - the function to interpolate between keyframes, defaults to
122
+ * {@link module:util.linearInterp}
123
+ * @property {string[]} interpolationKeys - keys to interpolate for objects, defaults to all
124
+ * own enumerable properties
125
+ */
126
+ export function val (element, path, time) {
127
+ if (hasCachedValue(element, path)) {
128
+ return getCachedValue(element, path)
129
+ }
130
+
131
+ // get property of element at path
132
+ const pathParts = path.split('.')
133
+ let property = element
134
+ while (pathParts.length > 0) {
135
+ property = property[pathParts.shift()]
136
+ }
137
+ const process = element.propertyFilters[path]
138
+
139
+ let value
140
+ if (isKeyFrames(property)) {
141
+ value = valKeyFrame(property, time)
142
+ } else if (typeof property === 'function') {
143
+ value = property(element, time) // TODO? add more args
144
+ } else {
145
+ value = property // simple value
146
+ }
147
+ return cacheValue(element, path, process ? process(value) : value)
148
+ }
149
+
150
+ function valKeyFrame (property, time) {
151
+ // if (Object.keys(property).length === 0) throw "Empty key frame set"; // this will never be executed
152
+ if (time === undefined) {
153
+ throw new Error('|time| is undefined or null')
154
+ }
155
+ // I think .reduce and such are slow to do per-frame (or more)?
156
+ // lower is the max beneath time, upper is the min above time
157
+ let lowerTime = 0; let upperTime = Infinity
158
+ let lowerValue = null; let upperValue = null // default values for the inequalities
159
+ for (let keyTime in property) {
160
+ const keyValue = property[keyTime]
161
+ keyTime = +keyTime // valueOf to convert to number
162
+
163
+ if (lowerTime <= keyTime && keyTime <= time) {
164
+ lowerValue = keyValue
165
+ lowerTime = keyTime
166
+ }
167
+ if (time <= keyTime && keyTime <= upperTime) {
168
+ upperValue = keyValue
169
+ upperTime = keyTime
170
+ }
171
+ }
172
+ // TODO: support custom interpolation for 'other' types
173
+ if (lowerValue === null) {
174
+ throw new Error(`No keyframes located before or at time ${time}.`)
175
+ }
176
+ // no need for upperValue if it is flat interpolation
177
+ if (!(typeof lowerValue === 'number' || typeof lowerValue === 'object')) {
178
+ return lowerValue
179
+ }
180
+ if (upperValue === null) {
181
+ throw new Error(`No keyframes located after or at time ${time}.`)
182
+ }
183
+ if (typeof lowerValue !== typeof upperValue) {
184
+ throw new Error('Type mismatch in keyframe values')
185
+ }
186
+ // interpolate
187
+ // the following should mean that there is a key frame *at* |time|; prevents division by zero below
188
+ if (upperTime === lowerTime) {
189
+ return upperValue
190
+ }
191
+ const progress = time - lowerTime; const percentProgress = progress / (upperTime - lowerTime)
192
+ const interpolate = property.interpolate || linearInterp
193
+ return interpolate(lowerValue, upperValue, percentProgress, property.interpolationKeys)
194
+ }
195
+
196
+ /* export function floorInterp(x1, x2, t, objectKeys) {
197
+ // https://stackoverflow.com/a/25835337/3783155 (TODO: preserve getters/setters, etc?)
198
+ return !objectKeys ? x1 : objectKeys.reduce((a, x) => {
199
+ if (x1.hasOwnProperty(x)) a[x] = o[x]; // ignore x2
200
+ return a;
201
+ }, Object.create(Object.getPrototypeOf(x1)));
202
+ } */
203
+
204
+ export function linearInterp (x1, x2, t, objectKeys) {
205
+ if (typeof x1 !== typeof x2) {
206
+ throw new Error('Type mismatch')
207
+ }
208
+ if (typeof x1 !== 'number' && typeof x1 !== 'object') {
209
+ return x1
210
+ } // flat interpolation (floor)
211
+ if (typeof x1 === 'object') { // to work with objects (including arrays)
212
+ // TODO: make this code DRY
213
+ if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2)) {
214
+ throw new Error('Prototype mismatch')
215
+ }
216
+ const int = Object.create(Object.getPrototypeOf(x1)) // preserve prototype of objects
217
+ // only take the union of properties
218
+ const keys = Object.keys(x1) || objectKeys
219
+ for (let i = 0; i < keys.length; i++) {
220
+ const key = keys[i]
221
+ // (only take the union of properties)
222
+ // eslint-disable-next-line no-prototype-builtins
223
+ if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key)) {
224
+ continue
225
+ }
226
+ int[key] = linearInterp(x1[key], x2[key], t)
227
+ }
228
+ return int
229
+ }
230
+ return (1 - t) * x1 + t * x2
231
+ }
232
+
233
+ export function cosineInterp (x1, x2, t, objectKeys) {
234
+ if (typeof x1 !== typeof x2) {
235
+ throw new Error('Type mismatch')
236
+ }
237
+ if (typeof x1 !== 'number' && typeof x1 !== 'object') {
238
+ return x1
239
+ } // flat interpolation (floor)
240
+ if (typeof x1 === 'object' && typeof x2 === 'object') { // to work with objects (including arrays)
241
+ if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2)) {
242
+ throw new Error('Prototype mismatch')
243
+ }
244
+ const int = Object.create(Object.getPrototypeOf(x1)) // preserve prototype of objects
245
+ // only take the union of properties
246
+ const keys = Object.keys(x1) || objectKeys
247
+ for (let i = 0; i < keys.length; i++) {
248
+ const key = keys[i]
249
+ // (only take the union of properties)
250
+ // eslint-disable-next-line no-prototype-builtins
251
+ if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key)) {
252
+ continue
253
+ }
254
+ int[key] = cosineInterp(x1[key], x2[key], t)
255
+ }
256
+ return int
257
+ }
258
+ const cos = Math.cos(Math.PI / 2 * t)
259
+ return cos * x1 + (1 - cos) * x2
260
+ }
261
+
262
+ /**
263
+ * An rgba color, for proper interpolation and shader effects
264
+ */
265
+ export class Color {
266
+ /**
267
+ * @param {number} r
268
+ * @param {number} g
269
+ * @param {number} b
270
+ * @param {number} a
271
+ */
272
+ constructor (r, g, b, a = 1.0) {
273
+ /** @type number */
274
+ this.r = r
275
+ /** @type number */
276
+ this.g = g
277
+ /** @type number */
278
+ this.b = b
279
+ /** @type number */
280
+ this.a = a
281
+ }
282
+
283
+ /**
284
+ * Converts to css color
285
+ */
286
+ toString () {
287
+ return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`
288
+ }
289
+ }
290
+
291
+ const parseColorCanvas = document.createElement('canvas')
292
+ parseColorCanvas.width = parseColorCanvas.height = 1
293
+ const parseColorCtx = parseColorCanvas.getContext('2d')
294
+ /**
295
+ * Converts a css color string to a {@link module:util.Color} object representation.
296
+ * @param {string} str
297
+ * @return {module:util.Color} the parsed color
298
+ */
299
+ export function parseColor (str) {
300
+ // TODO - find a better way to cope with the fact that invalid
301
+ // values of "col" are ignored
302
+ parseColorCtx.clearRect(0, 0, 1, 1)
303
+ parseColorCtx.fillStyle = str
304
+ parseColorCtx.fillRect(0, 0, 1, 1)
305
+ const data = parseColorCtx.getImageData(0, 0, 1, 1).data
306
+ return new Color(data[0], data[1], data[2], data[3] / 255)
307
+ }
308
+
309
+ /**
310
+ * A font, for proper interpolation
311
+ */
312
+ export class Font {
313
+ /**
314
+ * @param {number} size
315
+ * @param {string} family
316
+ * @param {string} sizeUnit
317
+ */
318
+ constructor (size, sizeUnit, family, style = 'normal', variant = 'normal',
319
+ weight = 'normal', stretch = 'normal', lineHeight = 'normal') {
320
+ this.size = size
321
+ this.sizeUnit = sizeUnit
322
+ this.family = family
323
+ this.style = style
324
+ this.variant = variant
325
+ this.weight = weight
326
+ this.stretch = stretch
327
+ this.lineHeight = lineHeight
328
+ }
329
+
330
+ /**
331
+ * Converts to css font syntax
332
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/font
333
+ */
334
+ toString () {
335
+ let s = ''
336
+ if (this.style !== 'normal') s += this.style + ' '
337
+ if (this.variant !== 'normal') s += this.variant + ' '
338
+ if (this.weight !== 'normal') s += this.weight + ' '
339
+ if (this.stretch !== 'normal') s += this.stretch + ' '
340
+ s += `${this.size}${this.sizeUnit} `
341
+ if (this.lineHeight !== 'normal') s += this.lineHeight + ' '
342
+ s += this.family
343
+
344
+ return s
345
+ }
346
+ }
347
+
348
+ const parseFontEl = document.createElement('div')
349
+ /**
350
+ * Converts a css font string to a {@link module:util.Font} object representation.
351
+ * @param {string} str
352
+ * @return {module:util.Font} the parsed font
353
+ */
354
+ export function parseFont (str) {
355
+ parseFontEl.setAttribute('style', `font: ${str}`) // assign css string to html element
356
+ const {
357
+ fontSize, fontFamily, fontStyle, fontVariant, fontWeight, lineHeight
358
+ } = parseFontEl.style
359
+ parseFontEl.removeAttribute('style')
360
+
361
+ const size = parseFloat(fontSize)
362
+ const sizeUnit = fontSize.substring(size.toString().length)
363
+ return new Font(size, sizeUnit, fontFamily, fontStyle, fontVariant, fontWeight, lineHeight)
364
+ }
365
+
366
+ /*
367
+ * Attempts to solve the diamond inheritance problem using mixins
368
+ * See {@link http://javascriptweblog.wordpress.com/2011/05/31/a-fresh-look-at-javascript-mixins/}<br>
369
+ *
370
+ * <strong>Note that the caller has to explicitly update the class value and as well as the class's property
371
+ * <code>constructor</code> to its prototype's constructor.</strong><br>
372
+ *
373
+ * This throws an error when composing functions with return values; unless if the composed function is a
374
+ * constructor, which is handled specially.
375
+ *
376
+ * Note that all properties must be functions for this to work as expected.
377
+ *
378
+ * If the destination and source have the methods with the same name (key), assign a new function
379
+ * that calls both with the given arguments. The arguments list passed to each subfunction will be the
380
+ * argument list that was called to the composite function.
381
+ *
382
+ * This function only works with functions, getters and setters.
383
+ *
384
+ * TODO: make a lot more robust
385
+ * TODO: rethink my ways... this is evil
386
+ */
387
+ /* export function extendProto(destination, source) {
388
+ for (let name in source) {
389
+ const extendMethod = (sourceDescriptor, which) => {
390
+ let sourceFn = sourceDescriptor[which],
391
+ origDestDescriptor = Object.getOwnPropertyDescriptor(destination, name),
392
+ origDestFn = origDestDescriptor ? origDestDescriptor[which] : undefined;
393
+ let destFn = !origDestFn ? sourceFn : function compositeMethod() { // `function` or `()` ?
394
+ try {
395
+ // |.apply()| because we're seperating the method from the object, so return the value
396
+ // of |this| back to the function
397
+ let r1 = origDestFn.apply(this, arguments),
398
+ r2 = sourceFn.apply(this, arguments);
399
+ if (r1 || r2) throw "Return value in composite method"; // null will slip by ig
400
+ } catch (e) {
401
+ if (e.toString() === "TypeError: class constructors must be invoked with |new|") {
402
+ let inst = new origDestFn(...arguments);
403
+ sourceFn.apply(inst, arguments);
404
+ return inst;
405
+ } else throw e;
406
+ }
407
+ };
408
+
409
+ let destDescriptor = {...sourceDescriptor}; // shallow clone
410
+ destDescriptor[which] = destFn;
411
+ Object.defineProperty(destination, name, destDescriptor);
412
+ };
413
+
414
+ let descriptor = Object.getOwnPropertyDescriptor(source, name);
415
+ if (descriptor) { // if hasOwnProperty
416
+ if (descriptor.get) extendMethod(descriptor, 'get');
417
+ if (descriptor.set) extendMethod(descriptor, 'set');
418
+ if (descriptor.value) extendMethod(descriptor, 'value');
419
+ }
420
+ }
421
+ } */
422
+
423
+ // TODO: remove this function
424
+ export function mapPixels (mapper, canvas, ctx, x, y, width, height, flush = true) {
425
+ x = x || 0
426
+ y = y || 0
427
+ width = width || canvas.width
428
+ height = height || canvas.height
429
+ const frame = ctx.getImageData(x, y, width, height)
430
+ for (let i = 0, l = frame.data.length; i < l; i += 4) {
431
+ mapper(frame.data, i)
432
+ }
433
+ if (flush) {
434
+ ctx.putImageData(frame, x, y)
435
+ }
436
+ }
437
+
438
+ /**
439
+ * <p>Emits "change" event when public properties updated, recursively
440
+ * <p>Must be called before any watchable properties are set, and only once in the prototype chain
441
+ *
442
+ * @param {object} target - object to watch
443
+ */
444
+ export function watchPublic (target) {
445
+ const getPath = (receiver, prop) =>
446
+ (receiver === proxy ? '' : (paths.get(receiver) + '.')) + prop
447
+ const callback = function (prop, val, receiver) {
448
+ // Public API property updated, emit 'modify' event.
449
+ publish(proxy, `${target.type}.change.modify`, { property: getPath(receiver, prop), newValue: val })
450
+ }
451
+ const check = prop => !(prop.startsWith('_') || target.publicExcludes.includes(prop))
452
+
453
+ const paths = new WeakMap() // the path to each child property (each is a unique proxy)
454
+
455
+ const handler = {
456
+ set (obj, prop, val, receiver) {
457
+ // Recurse
458
+ if (typeof val === 'object' && val !== null && !paths.has(val) && check(prop)) {
459
+ val = new Proxy(val, handler)
460
+ paths.set(val, getPath(receiver, prop))
461
+ }
462
+
463
+ const was = prop in obj
464
+ // set property or attribute
465
+ // Search prototype chain for the closest setter
466
+ let objProto = obj
467
+ while ((objProto = Object.getPrototypeOf(objProto))) {
468
+ const propDesc = Object.getOwnPropertyDescriptor(objProto, prop)
469
+ if (propDesc && propDesc.set) {
470
+ propDesc.set.call(receiver, val) // call setter, supplying proxy as this (fixes event bugs)
471
+ break
472
+ }
473
+ }
474
+ if (!objProto) { // couldn't find setter; set value on instance
475
+ obj[prop] = val
476
+ }
477
+ // Check if it already existed and if it's a valid property to watch, if on root object
478
+ if (obj !== target || (was && check(prop))) {
479
+ callback(prop, val, receiver)
480
+ }
481
+ return true
482
+ }
483
+ }
484
+
485
+ const proxy = new Proxy(target, handler)
486
+ return proxy
487
+ }