├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.custom-filters.md ├── README.md ├── dist ├── index.js └── index.js.map ├── easeljs ├── build.js └── easel-header.js ├── elixir.js ├── gulpfile.js ├── karma.conf.js ├── package.json ├── src ├── components │ ├── EaselBitmap.vue │ ├── EaselCanvas.vue │ ├── EaselContainer.vue │ ├── EaselShape.vue │ ├── EaselSprite.vue │ ├── EaselSpriteSheet.vue │ └── EaselText.vue ├── filters.js ├── filters │ ├── ColorMatrixFilter.js │ ├── FilterSet.js │ └── PixelStrokeFilter.js ├── index.js ├── libs │ ├── Events.js │ ├── PromiseParty.js │ ├── easel-event-binder.js │ ├── get-dimensions-from-get-bounds.js │ ├── normalize-alignment.js │ └── sort-by-dom.js └── mixins │ ├── EaselAlign.js │ ├── EaselCache.js │ ├── EaselDisplayObject.js │ ├── EaselFilter.js │ └── EaselParent.js └── test ├── ColorMatrixFilter.spec.js ├── EaselBitmap.spec.js ├── EaselCanvas.spec.js ├── EaselContainer.spec.js ├── EaselDisplayObject.spec.js ├── EaselShape.spec.js ├── EaselSprite.spec.js ├── EaselSpriteSheet.spec.js ├── EaselText.spec.js ├── Events.spec.js ├── FilterSet.spec.js ├── PixelStrokeFilter.spec.js ├── PromiseParty.spec.js ├── easel-event-binder.spec.js ├── fixtures ├── EaselFake.js └── Set.js ├── images ├── gulfstream_park.jpg └── lastguardian-all.png ├── includes ├── can-cache.js ├── can-filter.js ├── does-events.js ├── is-a-display-object.js ├── is-alignable.js └── is-an-easel-parent.js ├── normalize-alignment.spec.js ├── sort-by-dom.spec.js └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-object-rest-spread"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | sftp-config.json 2 | node_modules 3 | easeljs/easel.js 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 dankuck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.custom-filters.md: -------------------------------------------------------------------------------- 1 | ## Custom filters 2 | 3 | Create new filters by registering a class with the VueEaseljs library at 4 | runtime using the `VueEaseljs.registerFilter` method. 5 | 6 | | Parameter | | 7 | | ----- | ---- | 8 | | name | the name for the filter | 9 | | Filter | the class that filters | 10 | 11 | When the filter is used in an element's `filters` prop, the extra values are 12 | passed to the filter's constructor method. 13 | 14 | The filter should have one of two methods: either `adjustContext` or 15 | `adjustImageData`. 16 | 17 | ### adjustContext 18 | 19 | | Parameter | | 20 | | --------- | --- | 21 | | ctx | a CanvasRenderingContext2D that contains the visual element | 22 | | x | the x coordinate of the element on ctx | 23 | | y | the y coordinate of the element on ctx | 24 | | width | the width of the element on ctx | 25 | | height | the height of the element on ctx | 26 | | targetCtx | the CanvasRenderingContext2D to draw to, if absent, use ctx | 27 | | targetX | the x coordinate to draw to, if absent, use x | 28 | | targetY | the y coordinate to draw to, if absent, use y | 29 | 30 | This method should make changes to the data in `ctx` and write them to 31 | `targetCtx` if present, or else back to `ctx`. 32 | 33 | This method must return `true` if it succeeded. 34 | 35 | ### adjustImageData 36 | 37 | | Parameter | | 38 | | --------- | --- | 39 | | imageData | an ImageData object | 40 | 41 | This method should make changes directly to the `imageData` object. 42 | 43 | This method must return `true` if it succeeded. 44 | 45 | Example: 46 | ``` 47 | const VueEaseljs = require('vue-easeljs'); 48 | 49 | class MyFilter { 50 | 51 | constructor(value1, value2) { 52 | ... 53 | } 54 | 55 | adjustContext(ctx, x, y, width, height, targetCtx, targetX, targetY) { 56 | ... 57 | } 58 | } 59 | 60 | VueEaseljs.registerFilter('MyFilter', MyFilter); 61 | ``` 62 | -------------------------------------------------------------------------------- /easeljs/build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run this file to create easel.js by combining the easeljs repo files with 3 | * the header that makes it a proper ES6 module. 4 | * 5 | * This file is run automatically during `npm install`. 6 | */ 7 | 8 | const fs = require('fs'); 9 | 10 | /** 11 | * Only run in dev mode. In dev mode we install easeljs. Nobody else needs it. 12 | */ 13 | if (fs.existsSync(`${__dirname}/../node_modules/easeljs`)) { 14 | const header = fs.readFileSync(`${__dirname}/easel-header.js`); 15 | const body = fs.readFileSync(`${__dirname}/../node_modules/easeljs/lib/easeljs.js`); 16 | const whole = header + body; 17 | fs.writeFileSync(`${__dirname}/easel.js`, whole); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /easeljs/easel-header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This header gets attached to the code from the easeljs repo to create a 3 | * full, importable module. 4 | */ 5 | 6 | const createjs = this.createjs = this.createjs || {}; 7 | 8 | export default createjs; 9 | 10 | // If any content follows this line, it comes from the easeljs repo. 11 | // ---------------------------------------------------------------------------- 12 | 13 | -------------------------------------------------------------------------------- /elixir.js: -------------------------------------------------------------------------------- 1 | 2 | var elixir = require('laravel-elixir'); 3 | 4 | elixir.ready(function () { 5 | elixir.webpack.mergeConfig({ 6 | devtool: 'source-map', 7 | // ensure we are using the version of Vue that supports templates 8 | resolve: { 9 | alias: { 10 | vue: 'vue/dist/vue.common.js' 11 | }, 12 | extensions: ['.js', '.vue'] 13 | }, 14 | vue: { 15 | buble: { 16 | objectAssign: 'Object.assign' 17 | } 18 | }, 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.vue$/, 23 | loader: 'vue-loader' 24 | }, 25 | { 26 | test: /\.js$/, 27 | loader: 'buble-loader', 28 | query: { 29 | objectAssign: 'Object.assign', 30 | }, 31 | }, 32 | { 33 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 34 | loader: 'file-loader', 35 | query: { 36 | limit: 10000, 37 | name: '../img/[name].[hash:7].[ext]' 38 | } 39 | }, 40 | { 41 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 42 | loader: 'url-loader', 43 | query: { 44 | limit: 10000, 45 | name: '../fonts/[name].[hash:7].[ext]' 46 | } 47 | } 48 | ] 49 | } 50 | }) 51 | }); 52 | 53 | module.exports = elixir; 54 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var webpack = require('webpack'); 3 | 4 | var elixir = require('./elixir.js'); 5 | 6 | 7 | elixir.ready(function () { 8 | elixir.webpack.mergeConfig({ 9 | output: { 10 | libraryTarget: 'commonjs', 11 | }, 12 | plugins: [ 13 | new webpack.optimize.UglifyJsPlugin() 14 | ] 15 | }); 16 | }); 17 | 18 | elixir(function (mix) { 19 | mix.webpack('index.js', './dist', './src'); 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 2 | var elixir = require('./elixir.js'); 3 | 4 | var webpackConfig; 5 | 6 | elixir(function (mix) { 7 | webpackConfig = elixir.webpack.config; 8 | }); 9 | 10 | module.exports = function (config) { 11 | config.set({ 12 | browsers: ['PhantomJS'], 13 | frameworks: ['mocha'], 14 | files: [ 15 | 'test/test.js', 16 | { 17 | pattern: 'test/images/*', 18 | included: false, 19 | served: true, 20 | }, 21 | ], 22 | preprocessors: { 23 | 'test/test.js': ['webpack'], 24 | }, 25 | reporters: ['spec'], 26 | webpack: webpackConfig, 27 | webpackMiddleware: { 28 | noInfo: true, 29 | }, 30 | singleRun: true, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-easeljs", 3 | "version": "0.1.14", 4 | "description": "A Vue.js plugin to control an HTML5 canvas using EaselJS", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "karma start karma.conf.js", 8 | "build": "gulp", 9 | "postinstall": "node ./easeljs/build.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/dankuck/vue-easeljs.git" 14 | }, 15 | "contributors": { 16 | "name": "Dan Kuck-Alvarez", 17 | "email": "dankuck@gmail.com", 18 | "url": "http://www.dankuck.com/" 19 | }, 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/dankuck/vue-easeljs/issues" 23 | }, 24 | "homepage": "https://github.com/dankuck/vue-easeljs#readme", 25 | "devDependencies": { 26 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 27 | "easeljs": "^1.0.2", 28 | "gulp": "^3.9.1", 29 | "karma": "^1.7.0", 30 | "karma-mocha": "^1.3.0", 31 | "karma-phantomjs-launcher": "^1.0.4", 32 | "karma-spec-reporter": "0.0.32", 33 | "karma-webpack": "^2.0.4", 34 | "laravel-elixir": "^6.0.0-15", 35 | "laravel-elixir-vue-2": "^0.3.0", 36 | "laravel-elixir-webpack-official": "^1.0.10", 37 | "lodash.findindex": "^4.6.0", 38 | "lodash.intersection": "^4.4.0", 39 | "lodash.shuffle": "^4.2.0", 40 | "mocha": "^3.2.0", 41 | "phantomjs-prebuilt": "^2.1.14", 42 | "vue": "^2.1.*", 43 | "babel-plugin-transform-object-rest-spread": "^6.26.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/EaselBitmap.vue: -------------------------------------------------------------------------------- 1 | 42 | -------------------------------------------------------------------------------- /src/components/EaselCanvas.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 142 | -------------------------------------------------------------------------------- /src/components/EaselContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 40 | -------------------------------------------------------------------------------- /src/components/EaselShape.vue: -------------------------------------------------------------------------------- 1 | 105 | -------------------------------------------------------------------------------- /src/components/EaselSprite.vue: -------------------------------------------------------------------------------- 1 | 48 | -------------------------------------------------------------------------------- /src/components/EaselSpriteSheet.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | -------------------------------------------------------------------------------- /src/components/EaselText.vue: -------------------------------------------------------------------------------- 1 | 48 | -------------------------------------------------------------------------------- /src/filters.js: -------------------------------------------------------------------------------- 1 | import easeljs from '../easeljs/easel.js'; 2 | import FilterSet from './filters/FilterSet.js'; 3 | import ColorMatrixFilter from './filters/ColorMatrixFilter.js'; 4 | import PixelStrokeFilter from './filters/PixelStrokeFilter.js'; 5 | 6 | const filters = new FilterSet(); 7 | 8 | filters.register('BlurFilter', easeljs.BlurFilter); 9 | filters.register('ColorFilter', easeljs.ColorFilter); 10 | filters.register('ColorMatrixFilter', ColorMatrixFilter); 11 | filters.register('PixelStrokeFilter', PixelStrokeFilter); 12 | 13 | export default filters; 14 | -------------------------------------------------------------------------------- /src/filters/ColorMatrixFilter.js: -------------------------------------------------------------------------------- 1 | import easeljs from '../../easeljs/easel.js'; 2 | 3 | /** 4 | |------------------------ 5 | | ColorMatrixFilter 6 | |------------------------ 7 | | A version of the ColorMatrixFilter that accepts scalar constructor 8 | | parameters for ease-of-use. 9 | | 10 | | The constructor creates a ColorMatrix using the constructor params and 11 | | passes it to the EaselJS ColorMatrixFilter constructor. 12 | */ 13 | export default class ColorMatrixFilter extends easeljs.ColorMatrixFilter { 14 | 15 | constructor(brightness, contrast, saturation, hue) { 16 | const matrix = new easeljs.ColorMatrix(brightness, contrast, saturation, hue); 17 | easeljs.ColorMatrixFilter.apply(this, [matrix]); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/filters/FilterSet.js: -------------------------------------------------------------------------------- 1 | import easeljs from '../../easeljs/easel.js'; 2 | 3 | export default class FilterSet { 4 | 5 | constructor() { 6 | this.filters = []; 7 | } 8 | 9 | register(name, Filter) { 10 | if (Filter.prototype.applyFilter) { 11 | this.filters[name] = Filter; 12 | } else { 13 | const prototype = Filter.prototype || Filter.constructor.prototype; 14 | if (prototype.adjustContext) { 15 | prototype.usesContext = true; 16 | prototype.applyFilter = function (ctx, x, y, w, h, tctx, tx, ty) { 17 | return this.adjustContext(ctx, x, y, w, h, tctx, tx, ty); 18 | }; 19 | } else if (prototype.adjustImageData) { 20 | prototype.usesContext = false; 21 | prototype._applyFilter = function (imageData) { 22 | return this.adjustImageData(imageData); 23 | }; 24 | } else { 25 | throw new Error('Incompatible filter'); 26 | } 27 | for (let field in easeljs.Filter.prototype) { 28 | if (!prototype[field]) { 29 | prototype[field] = easeljs.Filter.prototype[field]; 30 | } 31 | } 32 | this.filters[name] = Filter; 33 | } 34 | } 35 | 36 | build(filterArray) { 37 | const filterName = filterArray[0]; 38 | const args = [null, ...filterArray.slice(1)]; 39 | const Filter = this.filters[filterName]; 40 | if (!Filter) { 41 | throw new Error(`No such filter registered: ${filterName}`); 42 | } 43 | return new (Function.prototype.bind.apply(Filter, args)); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/filters/PixelStrokeFilter.js: -------------------------------------------------------------------------------- 1 | import easeljs from '../../easeljs/easel.js'; 2 | 3 | /** 4 | |------------------------ 5 | | PixelStrokeFilter 6 | |------------------------ 7 | | Add a stroke to an element 8 | */ 9 | 10 | const calculatedBrushes = {}; 11 | 12 | export default class PixelStrokeFilter { 13 | 14 | constructor(stroke = [], size = 1, options = {}) { 15 | this.strokeRed = stroke[0] || 0; 16 | this.strokeGreen = stroke[1] || 0; 17 | this.strokeBlue = stroke[2] || 0; 18 | this.strokeAlpha = stroke[3] || 255; 19 | this.size = size; 20 | this.brush = this.calculateBrush(size); 21 | this.alphaCache = {}; 22 | this.options = options; 23 | } 24 | 25 | adjustImageData(imageData) { 26 | const {strokeRed, strokeGreen, strokeBlue, strokeAlpha} = this; 27 | const antiAlias = typeof this.options.antiAlias === 'undefined' ? true : this.options.antiAlias; 28 | const {data, width, height} = imageData; 29 | const length = data.length; 30 | const copy = data.slice(0); 31 | const activatePixel = (x, y, a) => { 32 | if (x < 0 || y < 0 || x >= width || y >= height) { 33 | return; 34 | } 35 | const i = (y * width + x) * 4; 36 | const alpha = antiAlias 37 | ? this.alphaCache[a] || (this.alphaCache[a] = Math.round(Math.floor(strokeAlpha * a))) 38 | : strokeAlpha; 39 | if (!copy[i + 3] && data[i + 3] < alpha) { 40 | data[i + 0] = strokeRed; 41 | data[i + 1] = strokeGreen; 42 | data[i + 2] = strokeBlue; 43 | data[i + 3] = alpha; 44 | } 45 | }; 46 | const applyBrush = (sx, sy) => { 47 | for (let i = 0; i < this.brush.length; i++) { 48 | const {y, minX, maxX, a} = this.brush[i]; 49 | activatePixel(sx + minX, sy + y, a); 50 | activatePixel(sx + maxX, sy + y, a); 51 | for (let j = minX + 1; j <= maxX - 1; j++) { 52 | activatePixel(sx + j, sy + y, 1); 53 | } 54 | } 55 | }; 56 | for (let x = 0; x < width; x++) { 57 | for (let y = 0; y < height; y++) { 58 | if (copy[(y * width + x) * 4 + 3] > 0) { 59 | applyBrush(x, y); 60 | } 61 | } 62 | } 63 | return true; 64 | } 65 | 66 | calculateBrush(size) { 67 | if (calculatedBrushes[size]) { 68 | return calculatedBrushes[size]; 69 | } 70 | /* 71 | Imagine a circle like this 72 | 1 | 2 73 | \ / 74 | 8 3 75 | - - 76 | 7 4 77 | / \ 78 | 6 | 5 79 | */ 80 | const map = {}; 81 | // Figure out sector 4 82 | for (let y = 0; y <= size; y++) { 83 | const tan = y / size; 84 | const angle = Math.atan(tan); 85 | const cos = Math.cos(angle); 86 | const sin = Math.sin(angle); 87 | const realX = cos * size; 88 | const realY = sin * size; 89 | const ceilX = Math.ceil(realX); 90 | const ceilY = Math.ceil(realY); 91 | const intensityX = realX - Math.floor(realX); 92 | const intensityY = realY - Math.floor(realY); 93 | let intensity = (intensityX + intensityY) / 2; 94 | if (intensity === 0) { 95 | intensity = 1; 96 | } 97 | map[ceilY] = { 98 | x: ceilX, 99 | y: ceilY, 100 | a: intensity, 101 | }; 102 | } 103 | // Flip sector 4's x and y to get sector 5 104 | for (let field in map) { 105 | const {x: y, y: x, a} = map[field]; 106 | if (!map[y]) { 107 | map[y] = { 108 | x, 109 | y, 110 | a, 111 | }; 112 | } 113 | } 114 | // Use that to build horizontal lines 115 | // from 7 to 4 116 | // from 6 to 5 117 | // from 1 to 2 118 | // from 8 to 3 119 | const lines = []; 120 | for (let field in map) { 121 | const {x, y, a} = map[field]; 122 | lines.push({ 123 | y, 124 | minX: -x, 125 | maxX: x, 126 | a, 127 | }); 128 | if (y !== 0) { 129 | lines.push({ 130 | y: -y, 131 | minX: -x, 132 | maxX: x, 133 | a, 134 | }); 135 | } 136 | } 137 | calculatedBrushes[size] = lines; 138 | return calculatedBrushes[size]; 139 | } 140 | 141 | getBounds(rect = null) { 142 | return (rect || new easeljs.Rectangle()).pad(this.size * 2, this.size * 2, this.size * 2, this.size * 2); 143 | } 144 | }; 145 | 146 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import easeljs from '../easeljs/easel.js'; 2 | import filters from './filters.js'; 3 | 4 | module.exports = { 5 | createjs: easeljs, 6 | easeljs: easeljs, 7 | EaselBitmap: require('./components/EaselBitmap.vue'), 8 | EaselCanvas: require('./components/EaselCanvas.vue'), 9 | EaselContainer: require('./components/EaselContainer.vue'), 10 | EaselShape: require('./components/EaselShape.vue'), 11 | EaselSprite: require('./components/EaselSprite.vue'), 12 | EaselSpriteSheet: require('./components/EaselSpriteSheet.vue'), 13 | EaselText: require('./components/EaselText.vue'), 14 | install(Vue) { 15 | Vue.component('easel-bitmap', this.EaselBitmap); 16 | Vue.component('easel-canvas', this.EaselCanvas); 17 | Vue.component('easel-container', this.EaselContainer); 18 | Vue.component('easel-shape', this.EaselShape); 19 | Vue.component('easel-sprite', this.EaselSprite); 20 | Vue.component('easel-sprite-sheet', this.EaselSpriteSheet); 21 | Vue.component('easel-text', this.EaselText); 22 | }, 23 | registerFilter(...args) { 24 | return filters.register(...args); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/libs/Events.js: -------------------------------------------------------------------------------- 1 | /** 2 | |--------------------- 3 | | Events 4 | |--------------------- 5 | | You need a basic event library. Here it is. 6 | | 7 | | Call events.on(string, Function) when you want it to send events 8 | | to . 9 | | 10 | | Call events.off(string, Function) when you want it to stop. 11 | | 12 | | Call events.fire(string, ...args) to call all the callbacks for 13 | | with the arguments that follow it. 14 | | 15 | */ 16 | export default class Events { 17 | 18 | constructor({errorCode} = {}) { 19 | this.callbacks = []; 20 | this.errorCode = errorCode; 21 | } 22 | 23 | on(event, cb) { 24 | this.callbacks.push({event, cb}); 25 | } 26 | 27 | off(removeEvent, removeCb) { 28 | this.callbacks = this.callbacks 29 | .filter(({event, cb}) => ! (event === removeEvent && cb === removeCb)); 30 | } 31 | 32 | fire(fireEvent, ...args) { 33 | this.callbacks 34 | .filter(({event}) => event === fireEvent) 35 | .forEach(({cb}) => { 36 | try { 37 | cb(...args) 38 | } catch (e) { 39 | if (this.errorCode && fireEvent !== this.errorCode) { 40 | this.fire(this.errorCode, fireEvent, args, cb, e); 41 | } 42 | } 43 | }); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/libs/PromiseParty.js: -------------------------------------------------------------------------------- 1 | import Events from './Events'; 2 | 3 | /** 4 | |------------------- 5 | | PromiseParty 6 | |------------------- 7 | | Send your Promises to a party and keep track of how many are there. Promises 8 | | leave the party when they resolve or reject. 9 | | 10 | | When a Promise is added, an 'add' event is fired, with the number of 11 | | Promises currently at the party. 12 | | 13 | | When a Promise completes (either resolves or rejects), a 'remove' event is 14 | | fired, with the number of Promises currently at the party. 15 | | 16 | | In both cases, a 'change' event is fired, and wouldn't you know it, it also 17 | | includes the number of Promises currently at the party. 18 | | 19 | | Use `on(, )` to listen to events. 20 | | 21 | | Use `off(, )` when you don't want to listen anymore. 22 | | 23 | | Use `add(Promise)` to send a new Promise to the party. 24 | | 25 | */ 26 | export default class PromiseParty { 27 | 28 | constructor() { 29 | this.events = new Events({errorCode: 'error'}); 30 | this.promises = new Set(); 31 | } 32 | 33 | add(promise) { 34 | if (!promise.finally) { 35 | throw new Error('We only accept promises here'); 36 | } 37 | 38 | this.promises.add(promise); 39 | this.events.fire('add', this.promises.size); 40 | this.events.fire('change', this.promises.size); 41 | 42 | promise.finally(() => { 43 | this.promises.delete(promise); 44 | this.events.fire('remove', this.promises.size); 45 | this.events.fire('change', this.promises.size); 46 | }); 47 | } 48 | 49 | on(event, cb) { 50 | this.events.on(event, cb); 51 | return this; 52 | } 53 | 54 | off(event, cb) { 55 | this.events.off(event, cb); 56 | return this; 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/libs/easel-event-binder.js: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | easel-event-binder.js 4 | |-------------------------------------------------------------------------- 5 | | 6 | | Binds all requested EaselJS events to a Vue component 7 | | 8 | */ 9 | 10 | // Need to add and test these events for Canvas 11 | // * drawend 12 | // * drawstart 13 | // * mouseenter 14 | // * mouseleave 15 | // * stagemousedown 16 | // * stagemousemove 17 | // * stagemouseup 18 | // * tickend 19 | // * tickstart 20 | 21 | import intersection from 'lodash.intersection'; 22 | 23 | export const eventTypes = [ 24 | 'added', 25 | 'animationend', 26 | 'change', 27 | 'click', 28 | 'dblclick', 29 | 'mousedown', 30 | 'mouseout', 31 | 'mouseover', 32 | 'pressmove', 33 | 'pressup', 34 | 'removed', 35 | 'rollout', 36 | 'rollover', 37 | 'tick', 38 | ]; 39 | 40 | const componentDemandsEventType = function (component, eventType) { 41 | return Boolean( 42 | component.$options._parentListeners 43 | && component.$options._parentListeners[eventType] 44 | ); 45 | }; 46 | 47 | const augmentEvent = function (component, event) { 48 | event.component = component; 49 | if (component.easelCanvas && component.easelCanvas.augmentEvent) { 50 | event = component.easelCanvas.augmentEvent(event); 51 | } 52 | return event; 53 | }; 54 | 55 | export default { 56 | bindEvents(vueComponent, easelComponent) { 57 | eventTypes.forEach(eventType => { 58 | if (!componentDemandsEventType(vueComponent, eventType)) { 59 | return; 60 | } 61 | easelComponent.addEventListener( 62 | eventType, 63 | (event) => vueComponent.$emit(eventType, augmentEvent(vueComponent, event)) 64 | ); 65 | }); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/libs/get-dimensions-from-get-bounds.js: -------------------------------------------------------------------------------- 1 | 2 | export default function getDimensionsFromGetBounds(component) { 3 | return new Promise((resolve, error) => { 4 | const getBounds = () => { 5 | try { 6 | if (!component.component) { 7 | // Component is uninitialized or went away, abandon. 8 | clearInterval(waiting); 9 | reject('No component available to getBounds'); 10 | } else if (component.component.getBounds()) { 11 | // Got the bounds, resolve with them 12 | clearInterval(waiting); 13 | const {x, y, width, height} = component.component.getBounds(); 14 | resolve({x, y, width, height}); 15 | } 16 | // else keep waiting... 17 | } catch (e) { 18 | // trouble! quit trying 19 | clearInterval(waiting); 20 | throw e; 21 | } 22 | } 23 | const waiting = setInterval(getBounds, 10); 24 | getBounds(); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /src/libs/normalize-alignment.js: -------------------------------------------------------------------------------- 1 | 2 | export const horizontalValues = ['', 'left', 'center', 'right', 'start', 'end']; 3 | export const verticalValues = ['', 'top', 'center', 'bottom', 'hanging', 'middle', 'alphabetic', 'ideographic']; 4 | 5 | const isHorizontal = value => horizontalValues.indexOf(value) > -1; 6 | const isVertical = value => verticalValues.indexOf(value) > -1; 7 | 8 | export default function normalizeAlignment(alignment) { 9 | if (typeof alignment === 'string') { 10 | alignment = alignment.trim().split(/\-/); 11 | } 12 | const [first, second] = alignment; 13 | if (isHorizontal(first) && isVertical(second)) { 14 | return [first, second]; 15 | } 16 | if (isVertical(first) && isHorizontal(second)) { 17 | return [second, first]; 18 | } 19 | throw new Error(`Illegal alignment, bad mix of values or unknown value in: ${first}, ${second}`); 20 | }; 21 | -------------------------------------------------------------------------------- /src/libs/sort-by-dom.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | |---------------------------- 4 | | sort-by-dom.js 5 | |---------------------------- 6 | | Given an array of Vue components, this will sort them in the same order 7 | | their elements are in the DOM. 8 | | 9 | */ 10 | 11 | export default function sortByDom(components) { 12 | return [...components].sort(sorter); 13 | }; 14 | 15 | export function sorter(a, b) { 16 | const compare = a.$el.compareDocumentPosition(b.$el); 17 | if (compare & Node.DOCUMENT_POSITION_DISCONNECTED) { 18 | throw new Error('Nodes are not in the same tree'); 19 | } 20 | if (compare & Node.DOCUMENT_POSITION_CONTAINS) { 21 | // b contains a, b should come first 22 | return 1; 23 | } 24 | if (compare & Node.DOCUMENT_POSITION_CONTAINED_BY) { 25 | // a contains b, a should come first 26 | return -1; 27 | } 28 | if (compare & Node.DOCUMENT_POSITION_PRECEDING) { 29 | // b precedes a, b should come first 30 | return 1; 31 | } 32 | if (compare & Node.DOCUMENT_POSITION_FOLLOWING) { 33 | // a precedes b, a should come first 34 | return -1; 35 | } 36 | // default to No Change 37 | return 0; 38 | }; 39 | -------------------------------------------------------------------------------- /src/mixins/EaselAlign.js: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | EaselAlign 4 | |-------------------------------------------------------------------------- 5 | | This mixin provides alignment support to a component. It handles the 6 | | `align` prop. 7 | | 8 | | Any component mixing this in should also mix in EaselDisplayObject. 9 | | 10 | | A component that mixes this in should provide: 11 | | * getAlignDimensions - A method that returns a Promise that resolves with 12 | | an object formatted as `{x, y, width, height}`. 13 | | 14 | */ 15 | 16 | import easeljs from '../../easeljs/easel.js'; 17 | import normalizeAlignment from '../libs/normalize-alignment.js'; 18 | 19 | export default { 20 | props: ['align'], 21 | watch: { 22 | align() { 23 | if (this.component) { 24 | this.updateAlign(); 25 | } 26 | }, 27 | }, 28 | mounted() { 29 | this.$watch('component', () => this.updateAlign()); 30 | }, 31 | computed: { 32 | /** 33 | * Normalizes the `align` prop's value by ensuring it is an array and 34 | * horizontal value comes before vertical value. 35 | * @return {Array} 36 | */ 37 | normalizedAlign() { 38 | return normalizeAlignment(this.align || ['', '']); 39 | }, 40 | }, 41 | methods: { 42 | /** 43 | * Sets the offset values for this element to those set by the align 44 | * prop. Returns a Promise that resolves with dimensions that were 45 | * passed to this method. 46 | * @return Promise 47 | */ 48 | updateAlign() { 49 | return this.remainInvisibleUntil( 50 | this.getAlignDimensions() 51 | // .then((dimensions) => this.$nextTick().then(() => dimensions)) 52 | .then( 53 | dimensions => { 54 | const w = dimensions.width, 55 | h = dimensions.height, 56 | hAlign = this.normalizedAlign[0] || 'left', 57 | vAlign = this.normalizedAlign[1] || 'top'; 58 | if (hAlign === 'left') { 59 | this.component.regX = 0; 60 | } else if (hAlign === 'center') { 61 | this.component.regX = w / 2; 62 | } else if (hAlign === 'right') { 63 | this.component.regX = w; 64 | } 65 | if (vAlign === 'top') { 66 | this.component.regY = 0; 67 | } else if (vAlign === 'center') { 68 | this.component.regY = h / 2; 69 | } else if (vAlign === 'bottom') { 70 | this.component.regY = h; 71 | } 72 | return dimensions; 73 | }, 74 | error => { 75 | console.error('Cannot align:', error); 76 | throw error; 77 | } 78 | ) 79 | ); 80 | }, 81 | /** 82 | * Returns a Promise that resolves with an object like 83 | * `{width, height}` so that alignment can be calculated. Subclasses 84 | * must define this method. 85 | * @return {Object} 86 | */ 87 | getAlignDimensions() { 88 | // Components should override this 89 | throw new Error('EaselAlign components must define a `getAlignDimensions` method'); 90 | }, 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /src/mixins/EaselCache.js: -------------------------------------------------------------------------------- 1 | /** 2 | |----------------------------------------------------------------------------- 3 | | EaselCache 4 | |----------------------------------------------------------------------------- 5 | | This mixin provides cache support to a component. It handles the `cache` 6 | | prop at initialization and any updates. 7 | | 8 | | It operates transparently, so it should be impossible to tell it's active 9 | | except that the code may operate more speedily. 10 | | 11 | | Any component mixing this in should also mix in EaselDisplayObject. 12 | | 13 | | A component that mixes this in should provide: 14 | | * updatesEaselCache - A top-level Array of the names of props or properties 15 | | to watch for changes that should trigger a cache 16 | | refresh 17 | | * getCacheBounds - A method that returns a Promise which resolves with 18 | | an object formatted as `{x, y, width, height}`. 19 | | 20 | */ 21 | 22 | export default { 23 | props: ['cache'], 24 | /** 25 | * Components that mix this in should replace this with an array of props 26 | * or properties that should trigger cache refreshes when they change. 27 | * @type {Array} 28 | */ 29 | updatesEaselCache: ['scale'], 30 | data() { 31 | return { 32 | cacheStarted: false, 33 | cacheNeedsUpdate: false, 34 | beforeCaches: [], 35 | cacheWhens: [], 36 | }; 37 | }, 38 | mounted() { 39 | this.updateCacheOnChange = () => { 40 | this.cacheNeedsUpdate = true; 41 | this.setParentCacheNeedsUpdate(); 42 | }; 43 | const setupOnChange = () => { 44 | if (this.component) { 45 | this.component.on('change', this.updateCacheOnChange); 46 | } 47 | }; 48 | window.addEventListener('resize', this.updateCacheOnChange); 49 | setupOnChange(); 50 | this.$watch('component', setupOnChange); 51 | this.$options.updatesEaselCache.forEach(prop => { 52 | this.$watch(prop, () => this.cacheNeedsUpdate = true); 53 | }); 54 | Object.keys(this.$options.props).forEach(prop => { 55 | this.$watch(prop, () => this.setParentCacheNeedsUpdate()); 56 | }); 57 | this.$nextTick(() => this.cacheInit()); 58 | }, 59 | destroyed() { 60 | window.removeEventListener('resize', this.updateCacheOnChange); 61 | }, 62 | watch: { 63 | shouldCache() { 64 | if (this.shouldCache) { 65 | this.cacheInit(); 66 | } else { 67 | this.cacheDestroy(); 68 | } 69 | }, 70 | cacheNeedsUpdate() { 71 | if (this.cacheNeedsUpdate && this.shouldCache) { 72 | this.$nextTick(() => { 73 | if (this.component && this.component.cacheCanvas) { 74 | this.cacheDestroy(); 75 | this.cacheInit(); 76 | // Didn't use updateCache() because it has a bug in 77 | // which it gives a new cache the same size as the 78 | // existing cache. 79 | } 80 | }); 81 | } 82 | }, 83 | }, 84 | computed: { 85 | shouldCache() { 86 | return this.cache 87 | || this.cacheWhens.reduce((result, callback) => result || callback(), false); 88 | }, 89 | cacheScale() { 90 | let scale = this.scale || 1; 91 | let parent = this.easelParent; 92 | while (parent) { 93 | if (parent.viewportScale) { 94 | scale *= parent.viewportScale.scaleX; 95 | } else { 96 | scale *= parent.scale || 1; 97 | } 98 | parent = parent.easelParent; 99 | } 100 | return scale; 101 | }, 102 | }, 103 | methods: { 104 | beforeCache(callback) { 105 | this.beforeCaches.push(callback); 106 | }, 107 | triggerBeforeCaches() { 108 | this.beforeCaches.forEach(callback => callback()); 109 | }, 110 | cacheWhen(callback) { 111 | this.cacheWhens.push(callback); 112 | }, 113 | cacheInit() { 114 | if (this.shouldCache) { 115 | this.getCacheBounds() 116 | .then(({x, y, width, height}) => { 117 | this.triggerBeforeCaches(); 118 | 119 | this.easelCanvas.createCanvas(() => { 120 | this.component.cache(x, y, width, height, this.cacheScale * window.devicePixelRatio); 121 | }); 122 | this.cacheStarted = true; 123 | this.cacheNeedsUpdate = false; 124 | }) 125 | .catch((error) => console.error(`Cannot cache: ${error}`, error)); 126 | } 127 | }, 128 | cacheDestroy() { 129 | this.component.uncache(); 130 | this.cacheStarted = false; 131 | this.cacheNeedsUpdate = false; 132 | }, 133 | setParentCacheNeedsUpdate() { 134 | if (this.easelParent && 'cacheNeedsUpdate' in this.easelParent) { 135 | this.easelParent.cacheNeedsUpdate = true; 136 | } 137 | }, 138 | /** 139 | * Get the bounds of a rectangle containing the element. This must be 140 | * defined by the subclass. It must return `{x, y, width, height}`. 141 | * These values are passed directly to EaselJS's cache() method. See 142 | * its documentation. 143 | * @return {Object} 144 | */ 145 | getCacheBounds() { 146 | return Promise.reject('EaselCache components must define a `getCacheBounds` method'); 147 | }, 148 | /** 149 | * Get the cache bounds as they would be seen from the parent's 150 | * perspective, and large enough to contain the element rotated to any 151 | * degree along with its shadow, if any. 152 | * EaselContainer uses this to calculate its own cache bounds. 153 | * @return {object} 154 | */ 155 | getRelativeCacheBounds() { 156 | return this.getCacheBounds() 157 | .then(bounds => { 158 | const x = ((this.x || 0) - this.component.regX) + bounds.x; 159 | const y = ((this.y || 0) - this.component.regY) + bounds.y; 160 | return { 161 | x, 162 | y, 163 | width: bounds.width, 164 | height: bounds.height, 165 | }; 166 | }) 167 | .then(bounds => this.expandForShadow(bounds)) 168 | .then(bounds => this.getSmallestSquare(bounds)) 169 | }, 170 | /** 171 | * Return the bounds of the smallest square that can contain the given 172 | * bounds rotated to any degree. 173 | * @param {Object} bounds 174 | * @return {Object} 175 | */ 176 | getSmallestSquare({x, y, width, height}) { 177 | // Use width and height as the legs of a right triangle and figure 178 | // out the hypotenuse. Thanks, Pythagoras. 179 | // The hypotenuse is the longest possible length that the object 180 | // could extend. 181 | const hypotenuse = Math.sqrt( 182 | Math.pow(width, 2) 183 | + Math.pow(height, 2) 184 | ); 185 | return { 186 | x: -hypotenuse, 187 | y: -hypotenuse, 188 | width: hypotenuse * 2, 189 | height: hypotenuse * 2, 190 | }; 191 | 192 | }, 193 | /** 194 | * Return the smallest bounds that can contain both bounds 195 | * @param {Object} a 196 | * @param {Object} b 197 | * @return {Object} 198 | */ 199 | getSmallestCombination(a, b) { 200 | const x = Math.min(a.x, b.x); 201 | const y = Math.min(a.y, b.y); 202 | const width = Math.max( 203 | (b.x - x) + b.width, 204 | (a.x - x) + a.width 205 | ); 206 | const height = Math.max( 207 | (b.y - y) + b.height, 208 | (a.y - y) + a.height 209 | ); 210 | return { 211 | x, 212 | y, 213 | width, 214 | height, 215 | }; 216 | }, 217 | /** 218 | * If a shadow exists, add its dimensions as padding on all sides 219 | * @return {Object} 220 | */ 221 | expandForShadow(bounds) { 222 | if (!this.shadow) { 223 | return bounds; 224 | } 225 | // Expand bounds to cover the shadow offsets and blurriness 226 | // in every direction. Needs to be every direction, since 227 | // rotation is applied before shadow. 228 | const [color, offsetX, offsetY, blurriness] = this.shadow; 229 | // Find the longest possible edge and expand the bounds in 230 | // every direction. We cannot be less naive, because we 231 | // want to account for every rotation. 232 | const padding = Math.max(offsetX, offsetY) + blurriness; 233 | return { 234 | x: bounds.x - padding, 235 | y: bounds.y - padding, 236 | width: bounds.width + padding * 2, 237 | height: bounds.height + padding * 2, 238 | }; 239 | }, 240 | }, 241 | }; 242 | -------------------------------------------------------------------------------- /src/mixins/EaselDisplayObject.js: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | EaselDisplayObject 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This mixin gives an Easel Vue component the required elements to be 7 | | visible on the canvas. 8 | | 9 | */ 10 | 11 | import EaselEventBinder from '../libs/easel-event-binder.js'; 12 | import easeljs from '../../easeljs/easel.js'; 13 | import PromiseParty from '../libs/PromiseParty.js'; 14 | 15 | const passthroughProps = ['rotation', 'cursor', 'name']; 16 | 17 | export default { 18 | inject: ['easelParent', 'easelCanvas'], 19 | props: { 20 | x: {}, 21 | y: {}, 22 | flip: {}, 23 | rotation: {}, 24 | scale: {}, 25 | alpha: {}, 26 | shadow: {}, 27 | cursor: {}, 28 | visible: { 29 | default: true, 30 | }, 31 | name: {}, 32 | }, 33 | data() { 34 | return { 35 | component: null, 36 | forceInvisiblePromises: new PromiseParty() 37 | .on('change', count => this.forceInvisible = count > 0), 38 | forceInvisible: false, 39 | }; 40 | }, 41 | mounted() { 42 | this.$watch('component', (now, old) => { 43 | if (old) { 44 | this.displayObjectBreakdown(old); 45 | } 46 | if (now) { 47 | this.displayObjectInit(); 48 | } 49 | }); 50 | // These just get copied directly onto the component; no funny business 51 | passthroughProps.forEach(prop => { 52 | this.$watch(prop, () => { 53 | if (this.component) { 54 | this.component[prop] = this[prop]; 55 | } 56 | }); 57 | }); 58 | this.$watch('x', () => { 59 | if (this.component) { 60 | this.component.x = this.x || 0; 61 | } 62 | }); 63 | this.$watch('y', () => { 64 | if (this.component) { 65 | this.component.y = this.y || 0; 66 | } 67 | }); 68 | this.$watch('flip', () => { 69 | if (this.component) { 70 | this.updateScales(); 71 | } 72 | }); 73 | this.$watch('scale', () => { 74 | if (this.component) { 75 | this.updateScales(); 76 | } 77 | }); 78 | this.$watch('alpha', () => { 79 | if (this.component) { 80 | this.updateAlpha(); 81 | } 82 | }); 83 | this.$watch('shadow', () => { 84 | if (this.component) { 85 | this.updateShadow(); 86 | } 87 | }); 88 | this.$watch('shouldBeVisible', () => { 89 | if (this.component) { 90 | this.updateVisibility(); 91 | } 92 | }); 93 | }, 94 | computed: { 95 | shouldBeVisible() { 96 | return this.visible && !this.forceInvisible; 97 | }, 98 | }, 99 | destroyed() { 100 | this.displayObjectBreakdown(); 101 | }, 102 | methods: { 103 | displayObjectInit() { 104 | EaselEventBinder.bindEvents(this, this.component); 105 | this.component.x = this.x || 0; 106 | this.component.y = this.y || 0; 107 | passthroughProps.forEach(prop => { 108 | this.component[prop] = this[prop]; 109 | }); 110 | this.updateScales(); 111 | this.updateAlpha(); 112 | this.updateShadow(); 113 | this.updateVisibility(); 114 | this.easelParent.addChild(this); 115 | }, 116 | displayObjectBreakdown(easelComponent = null) { 117 | this.easelParent.removeChild(this, easelComponent); 118 | }, 119 | updateScales() { 120 | if (this.component) { 121 | const scale = this.scale || 1; 122 | this.component.scaleX = this.flip === 'horizontal' || this.flip === 'both' ? -scale : scale; 123 | this.component.scaleY = this.flip === 'vertical' || this.flip === 'both' ? -scale : scale; 124 | } 125 | }, 126 | updateAlpha() { 127 | this.component.alpha = isNaN(this.alpha) || this.alpha === null ? 1 : this.alpha; 128 | }, 129 | updateShadow() { 130 | if (this.shadow) { 131 | this.component.shadow = new easeljs.Shadow(this.shadow[0], this.shadow[1], this.shadow[2], this.shadow[3]); 132 | } else { 133 | this.component.shadow = null; 134 | } 135 | }, 136 | updateVisibility() { 137 | this.component.visible = this.shouldBeVisible; 138 | }, 139 | /** 140 | * Force visible = false until this promise has resolved or rejected. 141 | * Returns a Promise that resolves when the given one does. 142 | * @param {Promise} promise 143 | * @return {Promise} 144 | */ 145 | remainInvisibleUntil(promise) { 146 | this.forceInvisiblePromises.add(promise); 147 | return promise; 148 | }, 149 | }, 150 | }; 151 | -------------------------------------------------------------------------------- /src/mixins/EaselFilter.js: -------------------------------------------------------------------------------- 1 | import easeljs from '../../easeljs/easel.js'; 2 | import filters from '../filters.js'; 3 | 4 | const build = filters.build.bind(filters); 5 | 6 | export default { 7 | props: ['filters'], 8 | mounted() { 9 | this.cacheWhen(() => this.filters && this.filters.length > 0); 10 | this.beforeCache(() => { 11 | if (this.filters && this.filters.length > 0) { 12 | this.component.filters = this.filters.map(build); 13 | } else { 14 | this.component.filters = null; 15 | } 16 | }); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/mixins/EaselParent.js: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | EaselParent 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This mixin lets a Vue component act as a container for an Easel Vue 7 | | component. 8 | | 9 | */ 10 | 11 | import easeljs from '../../easeljs/easel.js'; 12 | import sortByDom from '../libs/sort-by-dom.js'; 13 | import findIndex from 'lodash.findindex'; 14 | 15 | export default { 16 | provide() { 17 | return { 18 | easelParent: this, 19 | }; 20 | }, 21 | data() { 22 | return { 23 | // not guaranteed to be in order 24 | children: [], 25 | }; 26 | }, 27 | updated() { 28 | // runs when the DOM changes 29 | this.$nextTick(() => this.syncEaselChildren()); 30 | }, 31 | watch: { 32 | children() { 33 | this.syncEaselChildren(); 34 | }, 35 | }, 36 | methods: { 37 | syncEaselChildren() { 38 | if (this.component) { 39 | sortByDom(this.children).forEach((vueChild, i) => { 40 | const atPosition = this.component.numChildren >= i ? this.component.getChildAt(i) : null; 41 | if (vueChild.component === atPosition) { 42 | // already there 43 | return; 44 | } 45 | this.component.addChildAt(vueChild.component, i); 46 | }); 47 | } 48 | }, 49 | addChild(vueChild) { 50 | if (!this.hasChild(vueChild)) { 51 | this.children.push(vueChild); 52 | } 53 | }, 54 | removeChild(vueChild, easelComponent = null) { 55 | const index = this.indexOfChild(vueChild); 56 | if (index < 0) { 57 | return false; 58 | } 59 | this.children.splice(index, 1); 60 | if (this.component) { 61 | this.component.removeChild(easelComponent || vueChild.component); 62 | } 63 | return true; 64 | }, 65 | hasChild(vueChild) { 66 | return this.indexOfChild(vueChild) > -1; 67 | }, 68 | indexOfChild(vueChild) { 69 | return findIndex(this.children, vueChild); 70 | }, 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /test/ColorMatrixFilter.spec.js: -------------------------------------------------------------------------------- 1 | import ColorMatrixFilter from '../src/filters/ColorMatrixFilter.js'; 2 | import easeljs from '../easeljs/easel.js'; 3 | import assert from 'assert'; 4 | const {deepStrictEqual: equal} = assert; 5 | 6 | describe('ColorMatrixFilter', function () { 7 | 8 | it('instantiates', function () { 9 | new ColorMatrixFilter(); 10 | }); 11 | 12 | it('has a ColorMatrix', function () { 13 | const filter = new ColorMatrixFilter(1, 2, 3, 4); 14 | assert(filter.matrix); 15 | equal(filter.matrix.toArray(), new easeljs.ColorMatrix(1, 2, 3, 4).toArray()); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/EaselBitmap.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Vue from 'vue'; 3 | import EaselBitmap from '../src/components/EaselBitmap.vue'; 4 | import isADisplayObject from './includes/is-a-display-object.js'; 5 | import isAlignable from './includes/is-alignable.js'; 6 | import canCache from './includes/can-cache.js'; 7 | import canFilter from './includes/can-filter.js'; 8 | 9 | describe('EaselBitmap', function () { 10 | 11 | describe('is a display object that', isADisplayObject(EaselBitmap, 'image="/base/test/images/gulfstream_park.jpg"')); 12 | 13 | describe('is cacheable and', canCache(EaselBitmap, {}, [ 14 | { 15 | name: 'image', 16 | value: '/base/test/images/gulfstream_park.jpg', 17 | changeTo: '/base/test/images/lastguardian-all.png', 18 | shouldUpdateSameObject: true, 19 | }, 20 | ])); 21 | 22 | describe('is alignable and', isAlignable(EaselBitmap, {width: 1500, height: 946}, 'image="/base/test/images/gulfstream_park.jpg"')); 23 | 24 | describe('can filter and', canFilter(EaselBitmap, 'image="/base/test/images/gulfstream_park.jpg"')); 25 | 26 | const buildVm = function () { 27 | const easel = { 28 | addChild(vueChild) { 29 | }, 30 | removeChild(vueChild) { 31 | }, 32 | }; 33 | 34 | const vm = new Vue({ 35 | template: ` 36 | 37 | 44 | 45 | 46 | `, 47 | provide() { 48 | return { 49 | easelParent: easel, 50 | easelCanvas: easel, 51 | }; 52 | }, 53 | data() { 54 | return { 55 | image: '/base/test/images/gulfstream_park.jpg', 56 | showBitmap: true, 57 | align: 'top-left', 58 | }; 59 | }, 60 | components: { 61 | 'easel-bitmap': EaselBitmap, 62 | }, 63 | }).$mount(); 64 | 65 | const bitmap = vm.$refs.bitmap; 66 | 67 | return {vm, bitmap}; 68 | }; 69 | 70 | it('should exist', function () { 71 | const {vm, bitmap} = buildVm(); 72 | assert(bitmap); 73 | }); 74 | 75 | it('should have component field', function () { 76 | const {vm, bitmap} = buildVm(); 77 | assert(bitmap.component); 78 | }); 79 | 80 | it('should have the right image', function () { 81 | const {vm, bitmap} = buildVm(); 82 | assert(/gulfstream_park/.test(bitmap.component.image.src), 'Wrong src: ' + bitmap.component.image.src); 83 | }); 84 | 85 | it('should be able to change the image', function (done) { 86 | const {vm, bitmap} = buildVm(); 87 | const image = vm.image; 88 | vm.image = Math.random(); 89 | Vue.nextTick() 90 | .then(() => { 91 | const qr = new RegExp(vm.image); 92 | assert( 93 | qr.test(bitmap.component.image.src) || qr.test(bitmap.component.image), 94 | 'Wrong src in: ' + bitmap.component.image 95 | ); 96 | }) 97 | .then(done, done); 98 | }); 99 | 100 | it('should get dimensions', function (done) { 101 | const {vm, bitmap} = buildVm(); 102 | bitmap.getAlignDimensions() 103 | .then(dimensions => { 104 | assert(dimensions.width === 1500, 'Wrong width: ' + dimensions.width); 105 | assert(dimensions.height === 946, 'Wrong height: ' + dimensions.height); 106 | }) 107 | .then(done, done); 108 | }); 109 | 110 | ['center-left', 'top-left', 'bottom-right'] 111 | .forEach(align => { 112 | it('should get cache bounds (no matter the align)', function (done) { 113 | const {vm, bitmap} = buildVm(); 114 | vm.align = align; 115 | Vue.nextTick() 116 | .then(() => bitmap.getCacheBounds()) 117 | .then(({x, y, width, height}) => { 118 | assert(x === 0, `x is wrong: ${x}`); 119 | assert(y === 0, `y is wrong: ${y}`); 120 | assert(width === 1500, `width is wrong: ${width}`); 121 | assert(height === 946, `height is wrong: ${height}`); 122 | }) 123 | .then(done, done); 124 | }); 125 | }); 126 | 127 | it('updates alignment on image change', function (done) { 128 | const {vm, bitmap} = buildVm(); 129 | assert(bitmap.component.regX === 0, 'regX wrong at start'); 130 | assert(bitmap.component.regY === 0, 'regY wrong at start'); 131 | vm.align = 'center-center'; 132 | const image = new Image(); 133 | image.src = '/base/test/images/lastguardian-all.png'; 134 | image.addEventListener('load', function () { 135 | vm.image = image; 136 | Vue.nextTick() 137 | .then(() => { 138 | assert(bitmap.component.regX > 0, `regX now: ${bitmap.component.regX}`); 139 | assert(bitmap.component.regY > 0, `regY now: ${bitmap.component.regY}`); 140 | }) 141 | .then(done, done); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /test/EaselCanvas.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Vue from 'vue'; 3 | import EaselCanvas from '../src/components/EaselCanvas.vue'; 4 | import easeljs from '../easeljs/easel.js'; 5 | import isAnEaselParent from './includes/is-an-easel-parent.js'; 6 | import doesEvents from './includes/does-events.js'; 7 | 8 | describe('EaselCanvas', function () { 9 | 10 | describe('is an easel parent that', isAnEaselParent(EaselCanvas)); 11 | 12 | describe('does events and', doesEvents(EaselCanvas)); 13 | 14 | const buildVm = function () { 15 | const vm = new Vue({ 16 | template: ` 17 | 26 | 27 | 28 | `, 29 | data() { 30 | return { 31 | eventLog: [], 32 | antiAlias: true, 33 | height: 300, 34 | width: 400, 35 | vheight: null, 36 | vwidth: null, 37 | }; 38 | }, 39 | components: { 40 | 'easel-canvas': EaselCanvas, 41 | }, 42 | methods: { 43 | logEvent(event) { 44 | this.eventLog.push(event); 45 | }, 46 | clearEventLog() { 47 | this.eventLog = []; 48 | }, 49 | }, 50 | }).$mount(); 51 | 52 | const canvas = vm.$refs.easelCanvas; 53 | 54 | return {vm, canvas}; 55 | }; 56 | 57 | it('should have a canvas object', function () { 58 | const {vm, canvas} = buildVm(); 59 | assert(vm.$el.nodeName === 'CANVAS'); 60 | }); 61 | 62 | it('should have the slot stuff we put in', function () { 63 | const {vm, canvas} = buildVm(); 64 | assert(vm.$el.querySelector('#im-in-a-slot')); 65 | }); 66 | 67 | it('should have an easel object with a component object', function () { 68 | const {vm, canvas} = buildVm(); 69 | assert(canvas.component); 70 | }); 71 | 72 | it('should have a component that calls update on its own', function (done) { 73 | const {vm, canvas} = buildVm(); 74 | const update = canvas.component.update; 75 | canvas.component.update = function (event) { 76 | assert(event); 77 | canvas.component.update = update; 78 | done(); 79 | }; 80 | }); 81 | 82 | it('should be able to anti-alias', function (done) { 83 | const {vm, canvas} = buildVm(); 84 | Vue.nextTick() 85 | .then(() => { 86 | assert(canvas.context.imageSmoothingEnabled === true, 'Not smoothing: ' + canvas.context.imageSmoothingEnabled); 87 | }) 88 | .then(done, done); 89 | }); 90 | 91 | it('should not use anti-alias', function (done) { 92 | const {vm, canvas} = buildVm(); 93 | vm.antiAlias = false; 94 | Vue.nextTick() 95 | .then(() => { 96 | assert(canvas.context.imageSmoothingEnabled === false, 'Smoothing, but should not: ' + canvas.context.imageSmoothingEnabled); 97 | }) 98 | .then(done, done); 99 | }); 100 | 101 | it('should keep anti-alias off after resize', function (done) { 102 | const {vm, canvas} = buildVm(); 103 | vm.antiAlias = false; 104 | Vue.nextTick() 105 | .then(() => { 106 | assert(canvas.context.imageSmoothingEnabled === false, 'Smoothing, but should not: ' + canvas.context.imageSmoothingEnabled); 107 | canvas.context.imageSmoothingEnabled = true; 108 | window.dispatchEvent(new Event('resize')); 109 | return Vue.nextTick(); 110 | }) 111 | .then(() => { 112 | assert(canvas.context.imageSmoothingEnabled === false, 'Smoothing again, but should not: ' + canvas.context.imageSmoothingEnabled); 113 | }) 114 | .then(done, done); 115 | }); 116 | 117 | it('should default anti-alias to true', function (done) { 118 | const {vm, canvas} = buildVm(); 119 | vm.antiAlias = undefined; 120 | Vue.nextTick() 121 | .then(() => { 122 | assert(canvas.context.imageSmoothingEnabled === true, 'Not smoothing, but should: ' + canvas.context.imageSmoothingEnabled); 123 | }) 124 | .then(done, done); 125 | }); 126 | 127 | it('should provide canvases to any code it wraps', function () { 128 | const {vm, canvas} = buildVm(); 129 | canvas.createCanvas(() => { 130 | assert(easeljs.createCanvas, 'createCanvas does not exist'); 131 | }); 132 | assert(!easeljs.createCanvas, 'createCanvas exists'); 133 | }); 134 | 135 | it('should provide different canvases to any code it wraps twice', function () { 136 | const {vm, canvas} = buildVm(); 137 | let firstCreateCanvas, 138 | secondCreateCanvas, 139 | firstCreateCanvasAgain; 140 | canvas.createCanvas(() => { 141 | firstCreateCanvas = easeljs.createCanvas; 142 | canvas.createCanvas(() => { 143 | secondCreateCanvas = easeljs.createCanvas; 144 | }); 145 | firstCreateCanvasAgain = easeljs.createCanvas; 146 | }); 147 | assert(firstCreateCanvas, 'firstCreateCanvas should exist'); 148 | assert(secondCreateCanvas, 'secondCreateCanvas should exist'); 149 | assert(firstCreateCanvasAgain, 'firstCreateCanvasAgain should exist'); 150 | assert(secondCreateCanvas !== firstCreateCanvas, '1st and 2nd should not be the same'); 151 | assert(firstCreateCanvas === firstCreateCanvasAgain, '1st and 3rd should be the same'); 152 | assert(!easeljs.createCanvas, 'createCanvas should not exist'); 153 | }); 154 | 155 | it('should scale to device pixel ratio', function () { 156 | window.devicePixelRatio = 2; 157 | const {vm, canvas} = buildVm(); 158 | const htmlCanvas = canvas.$refs.canvas; 159 | assert(htmlCanvas.width === 800, `${htmlCanvas.width} !== 800`); 160 | assert(htmlCanvas.height === 600, `${htmlCanvas.height} !== 600`); 161 | assert(htmlCanvas.style.width === '400px', `${htmlCanvas.style.width} !== 400px`); 162 | assert(htmlCanvas.style.height === '300px', `${htmlCanvas.style.height} !== 300px`); 163 | assert(canvas.component.scale === 2, `${canvas.component.scale} !== 2`); 164 | }); 165 | 166 | it('should rescale on device pixel ratio change', function (done) { 167 | window.devicePixelRatio = 2; 168 | const {vm, canvas} = buildVm(); 169 | const htmlCanvas = canvas.$refs.canvas; 170 | window.devicePixelRatio = 3; 171 | window.dispatchEvent(new Event('resize')); 172 | Vue.nextTick() 173 | .then(() => { 174 | assert(htmlCanvas.width === 1200, `${htmlCanvas.width} !== 1200`); 175 | assert(htmlCanvas.height === 900, `${htmlCanvas.height} !== 900`); 176 | assert(htmlCanvas.style.width === '400px', `${htmlCanvas.style.width} !== 400px`); 177 | assert(htmlCanvas.style.height === '300px', `${htmlCanvas.style.height} !== 300px`); 178 | assert(canvas.component.scale === 3, `${canvas.component.scale} !== 3`); 179 | }) 180 | .then(done, done); 181 | }); 182 | 183 | it('should rescale on width and height change', function (done) { 184 | window.devicePixelRatio = 2; 185 | const {vm, canvas} = buildVm(); 186 | const htmlCanvas = canvas.$refs.canvas; 187 | vm.height = 301; 188 | Vue.nextTick() 189 | .then(() => { 190 | assert(htmlCanvas.width === 800, `${htmlCanvas.width} !== 800`); 191 | assert(htmlCanvas.height === 602, `${htmlCanvas.height} !== 602`); 192 | assert(htmlCanvas.style.width === '400px', `${htmlCanvas.style.width} !== 400px`); 193 | assert(htmlCanvas.style.height === '301px', `${htmlCanvas.style.height} !== 301px`); 194 | assert(canvas.component.scale === 2, `${canvas.component.scale} !== 2`); 195 | vm.width = 401; 196 | return Vue.nextTick(); 197 | }) 198 | .then(() => { 199 | assert(htmlCanvas.width === 802, `${htmlCanvas.width} !== 802`); 200 | assert(htmlCanvas.height === 602, `${htmlCanvas.height} !== 602`); 201 | assert(htmlCanvas.style.width === '401px', `${htmlCanvas.style.width} !== 401px`); 202 | assert(htmlCanvas.style.height === '301px', `${htmlCanvas.style.height} !== 301px`); 203 | assert(canvas.component.scale === 2, `${canvas.component.scale} !== 2`); 204 | }) 205 | .then(done, done); 206 | }); 207 | 208 | it('should have viewport-height and viewport-width', function (done) { 209 | window.devicePixelRatio = 1; 210 | const {vm, canvas} = buildVm(); 211 | vm.vheight = 300; 212 | vm.vwidth = 400; 213 | Vue.nextTick() 214 | .then(() => { 215 | assert(canvas.viewportHeight === 300); 216 | assert(canvas.viewportWidth === 400); 217 | assert(canvas.component.scaleY === 1); 218 | assert(canvas.component.scaleX === 1); 219 | }) 220 | .then(done, done); 221 | }); 222 | 223 | it('should change scaleX and scaleY with viewport-height and viewport-width', function (done) { 224 | window.devicePixelRatio = 1; 225 | const {vm, canvas} = buildVm(); 226 | vm.vheight = 600; // twice the canvas height 227 | vm.vwidth = 200; // half the canvas width 228 | Vue.nextTick() 229 | .then(() => { 230 | assert(canvas.component.scaleY === .5, JSON.stringify([canvas.viewport, canvas.viewportScale, canvas.component.scaleY, canvas.viewportHeight])); 231 | assert(canvas.component.scaleX === 2); 232 | }) 233 | .then(done, done); 234 | }); 235 | 236 | it('should change scaleX and scaleY with viewport-height and viewport-width', function (done) { 237 | window.devicePixelRatio = 1; 238 | const {vm, canvas} = buildVm(); 239 | // cause scale to double 240 | vm.vheight = 150; 241 | vm.vwidth = 200; 242 | Vue.nextTick() 243 | .then(() => { 244 | // cause scale to double again 245 | window.devicePixelRatio = 2; 246 | window.dispatchEvent(new Event('resize')); 247 | return Vue.nextTick(); 248 | }) 249 | .then(() => { 250 | assert(canvas.component.scaleY === 4); 251 | assert(canvas.component.scaleX === 4); 252 | }) 253 | .then(done, done); 254 | }); 255 | 256 | it('should have touch', function () { 257 | const Touch = easeljs.Touch; 258 | let sawEnable, sawDisable; 259 | easeljs.Touch = { 260 | enable(component) { 261 | sawEnable = component; 262 | }, 263 | disable(component) { 264 | sawDisable = component; 265 | }, 266 | }; 267 | const vm = new Vue({ 268 | template: ` 269 | 270 | `, 271 | components: { 272 | 'easel-canvas': EaselCanvas, 273 | }, 274 | }).$mount(); 275 | vm.$destroy(); 276 | assert(sawEnable, 'did not see enable'); 277 | assert(sawDisable, 'did not see disable'); 278 | easeljs.Touch = Touch; 279 | }); 280 | 281 | it('should augment events', function () { 282 | window.devicePixelRatio = 2; 283 | const {vm, canvas} = buildVm(); 284 | const event = canvas.augmentEvent({ 285 | stageX: 100, 286 | stageY: 200, 287 | }); 288 | assert(event.viewportX === 50); 289 | assert(event.viewportY === 100); 290 | }); 291 | }); 292 | -------------------------------------------------------------------------------- /test/EaselContainer.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import EaselContainer from '../src/components/EaselContainer.vue'; 3 | import easeljs from '../easeljs/easel.js'; 4 | import Vue from 'vue'; 5 | import isAnEaselParent from './includes/is-an-easel-parent.js'; 6 | import EaselFake from './fixtures/EaselFake.js'; 7 | import isADisplayObject from './includes/is-a-display-object.js'; 8 | import canCache from './includes/can-cache.js'; 9 | import canFilter from './includes/can-filter.js'; 10 | const {deepStrictEqual: equal} = assert; 11 | 12 | describe('EaselContainer', function () { 13 | 14 | describe('is an easel parent that', isAnEaselParent(EaselContainer)); 15 | 16 | describe('is a display object that', isADisplayObject(EaselContainer)); 17 | 18 | describe('is cacheable and', canCache(EaselContainer, {}, [])); 19 | 20 | describe('can filter and', canFilter(EaselContainer)); 21 | 22 | const buildVm = function () { 23 | const easel = { 24 | addChild(vueChild) { 25 | }, 26 | removeChild(vueChild) { 27 | }, 28 | }; 29 | 30 | const vm = new Vue({ 31 | template: ` 32 | 33 | 39 | 40 | 41 | `, 42 | provide() { 43 | return { 44 | easelParent: easel, 45 | easelCanvas: easel, 46 | }; 47 | }, 48 | data() { 49 | return { 50 | showFake: true, 51 | x: 3, 52 | y: 4, 53 | shadow: null, 54 | }; 55 | }, 56 | components: { 57 | 'easel-fake': EaselFake, 58 | 'easel-container': EaselContainer, 59 | }, 60 | }).$mount(); 61 | 62 | const container = vm.$refs.container; 63 | const fake = vm.$refs.fake; 64 | 65 | return {vm, container, fake}; 66 | }; 67 | 68 | it('should exist', function () { 69 | const {vm, container, fake} = buildVm(); 70 | assert(container); 71 | }); 72 | 73 | it('should have an easel', function () { 74 | const {vm, container, fake} = buildVm(); 75 | assert(container.easelParent); 76 | }); 77 | 78 | it('should have component field', function () { 79 | const {vm, container, fake} = buildVm(); 80 | assert(container.component); 81 | }); 82 | 83 | it('should be the parent of the fake', function (done) { 84 | const {vm, container, fake} = buildVm(); 85 | Vue.nextTick() 86 | .then(() => { 87 | assert(fake.component.parent === container.component); 88 | }) 89 | .then(done, done); 90 | }); 91 | 92 | it('should get cache dimensions including the fake', function (done) { 93 | const {vm, container, fake} = buildVm(); 94 | Vue.nextTick() 95 | .then(() => { 96 | vm.x = 0; 97 | vm.y = 0; 98 | return Vue.nextTick(); 99 | }) 100 | .then(() => { 101 | return Promise.all([ 102 | fake.getCacheBounds(), 103 | container.getCacheBounds(), 104 | ]); 105 | }) 106 | .then(([fakeBounds, containerBounds]) => { 107 | // from EaselFake fixture 108 | equal( 109 | { 110 | x: -10, 111 | y: -20, 112 | width: 30, 113 | height: 40, 114 | }, 115 | fakeBounds 116 | ); 117 | // The square that surrounds a rotating EaselFake 118 | // The square is calculated using the hypotenuse of the above 119 | // rectangle. 120 | // √(30² + 40²) = 50, a super-convenient exact number 121 | equal( 122 | { 123 | x: -50, 124 | y: -50, 125 | width: 100, 126 | height: 100, 127 | }, 128 | { 129 | x: containerBounds.x, 130 | y: containerBounds.y, 131 | width: containerBounds.width, 132 | height: containerBounds.height, 133 | } 134 | ); 135 | }) 136 | .then(done, done); 137 | }); 138 | 139 | it('should get cache dimensions including the shadow', function (done) { 140 | const {vm, container, fake} = buildVm(); 141 | Vue.nextTick() 142 | .then(() => { 143 | vm.x = 0; 144 | vm.y = 0; 145 | vm.shadow = ['black', 5, 10, 5]; 146 | return Vue.nextTick(); 147 | }) 148 | .then(() => { 149 | return Promise.all([ 150 | fake.getCacheBounds(), 151 | container.getCacheBounds(), 152 | ]); 153 | }) 154 | .then(([fakeBounds, containerBounds]) => { 155 | // from EaselFake fixture 156 | equal( 157 | { 158 | x: -10, 159 | y: -20, 160 | width: 30, 161 | height: 40, 162 | }, 163 | fakeBounds 164 | ); 165 | // The square that surrounds a rotating EaselFake 166 | // First the shadow's padding is added to all sides. 167 | // The shadow padding is done by adding its longest offset and 168 | // the blurriness amount. In this case 10 and 5. Since it's on 169 | // all sides, it's applied twice to width and height. That 170 | // makes an addition of 30 to both. 171 | // The square is calculated using the hypotenuse of the 172 | // resulting rectangle. 173 | // √((30 + 30)² + (40 + 30)²) ~= 92 174 | equal( 175 | { 176 | x: -92, 177 | y: -92, 178 | width: 184, 179 | height: 184, 180 | }, 181 | { 182 | x: Math.round(containerBounds.x), 183 | y: Math.round(containerBounds.y), 184 | width: Math.round(containerBounds.width), 185 | height: Math.round(containerBounds.height), 186 | } 187 | ); 188 | }) 189 | .then(done, done); 190 | }); 191 | 192 | it('should update cache when children disappear', function (done) { 193 | const {vm, container, fake} = buildVm(); 194 | Vue.nextTick() 195 | .then(() => { 196 | container.cacheNeedsUpdate = false; 197 | vm.showFake = false; 198 | return Vue.nextTick(); 199 | }) 200 | .then(() => { 201 | assert(container.cacheNeedsUpdate); 202 | }) 203 | .then(done, done); 204 | }); 205 | 206 | it('should update cache when children reappear', function (done) { 207 | const {vm, container, fake} = buildVm(); 208 | Vue.nextTick() 209 | .then(() => { 210 | vm.showFake = false; 211 | return Vue.nextTick(); 212 | }) 213 | .then(() => { 214 | container.cacheNeedsUpdate = false; 215 | vm.showFake = true; 216 | return Vue.nextTick(); 217 | }) 218 | .then(() => { 219 | assert(container.cacheNeedsUpdate); 220 | }) 221 | .then(done, done); 222 | }); 223 | }); 224 | -------------------------------------------------------------------------------- /test/EaselDisplayObject.spec.js: -------------------------------------------------------------------------------- 1 | import EaselFake from './fixtures/EaselFake.js'; 2 | import isADisplayObject from './includes/is-a-display-object.js'; 3 | 4 | describe('EaselDisplayObject', function () { 5 | 6 | describe('is a display object that', isADisplayObject(EaselFake)); 7 | }); 8 | -------------------------------------------------------------------------------- /test/EaselSprite.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Vue from 'vue'; 3 | import EaselSprite from '../src/components/EaselSprite.vue'; 4 | import easeljs from '../easeljs/easel.js'; 5 | import isADisplayObject from './includes/is-a-display-object.js'; 6 | import canCache from './includes/can-cache.js'; 7 | import isAlignable from './includes/is-alignable.js'; 8 | import canFilter from './includes/can-filter.js'; 9 | 10 | const garyStart = 32 * 6 + 16; 11 | 12 | const spriteSheet = new easeljs.SpriteSheet({ 13 | images: ['/base/test/images/lastguardian-all.png'], 14 | frames: {width: 32, height: 32}, 15 | animations: { 16 | stand: garyStart + 5, 17 | run: [garyStart + 6, garyStart + 7], 18 | }, 19 | framerate: 30, 20 | }); 21 | 22 | describe('EaselSprite', function () { 23 | 24 | describe('is a display object that', isADisplayObject(EaselSprite, '', {spriteSheet})); 25 | 26 | describe('is cacheable and', canCache(EaselSprite, {spriteSheet}, [ 27 | { 28 | name: 'animation', 29 | value: 'stand', 30 | changeTo: 'run', 31 | shouldUpdateSameObject: true, 32 | }, 33 | ])); 34 | 35 | describe('can filter and', canFilter(EaselSprite, '', {spriteSheet})); 36 | 37 | describe('is alignable and', isAlignable(EaselSprite, {width: 32, height: 32}, '', {spriteSheet})); 38 | 39 | const buildVm = function () { 40 | const easel = { 41 | addChild(vueChild) { 42 | }, 43 | removeChild(vueChild) { 44 | }, 45 | }; 46 | 47 | const vm = new Vue({ 48 | template: ` 49 | 50 | 58 | 59 | 60 | `, 61 | provide() { 62 | return { 63 | spriteSheet, 64 | easelParent: easel, 65 | easelCanvas: easel, 66 | }; 67 | }, 68 | data() { 69 | return { 70 | animation: 'stand', 71 | x: 1, 72 | y: 2, 73 | showSprite: true, 74 | flip: '', 75 | align: 'top-left', 76 | }; 77 | }, 78 | components: { 79 | 'easel-sprite': EaselSprite, 80 | }, 81 | }).$mount(); 82 | 83 | const sprite = vm.$refs.sprite; 84 | 85 | return {vm, sprite}; 86 | }; 87 | 88 | it('should exist', function () { 89 | const {vm, sprite} = buildVm(); 90 | assert(sprite); 91 | }); 92 | 93 | it('should have a spritesheet', function () { 94 | const {vm, sprite} = buildVm(); 95 | assert(sprite.spriteSheet); 96 | }); 97 | 98 | it('should have component field', function () { 99 | const {vm, sprite} = buildVm(); 100 | assert(sprite.component); 101 | }); 102 | 103 | it('should run `stand` animation', function () { 104 | const {vm, sprite} = buildVm(); 105 | assert(sprite.component._animation && sprite.component._animation.name === 'stand'); 106 | }); 107 | 108 | it('should change animation to `run`', function (done) { 109 | const {vm, sprite} = buildVm(); 110 | vm.animation = 'run'; 111 | Vue.nextTick() 112 | .then(() => { 113 | assert(sprite.component._animation && sprite.component._animation.name === 'run'); 114 | }) 115 | .then(done, done); 116 | }); 117 | 118 | it('should get dimensions', function (done) { 119 | const {vm, sprite} = buildVm(); 120 | sprite.getAlignDimensions() 121 | .then(dimensions => { 122 | assert(dimensions.width === 32); 123 | assert(dimensions.height === 32); 124 | }) 125 | .then(done, done); 126 | }); 127 | 128 | ['center-left', 'top-left', 'bottom-right'] 129 | .forEach(align => { 130 | it('should get cache bounds (no matter the align)', function (done) { 131 | const {vm, sprite} = buildVm(); 132 | vm.align = align; 133 | Vue.nextTick() 134 | .then(() => sprite.getCacheBounds()) 135 | .then(({x, y, width, height}) => { 136 | assert(x === 0, `x is wrong: ${x}`); 137 | assert(y === 0, `y is wrong: ${y}`); 138 | assert(width === 32, `width is wrong: ${width}`); 139 | assert(height === 32, `height is wrong: ${height}`); 140 | }) 141 | .then(done, done); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /test/EaselSpriteSheet.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Vue from 'vue'; 3 | import EaselSpriteSheet from '../src/components/EaselSpriteSheet.vue'; 4 | import easeljs from '../easeljs/easel.js'; 5 | 6 | describe('EaselSpriteSheet', function () { 7 | 8 | const buildVm = function () { 9 | const vm = new Vue({ 10 | template: ` 11 | 17 | 18 | 19 | `, 20 | components: { 21 | 'easel-sprite-sheet': EaselSpriteSheet, 22 | 'x-inject': { 23 | inject: ['spriteSheet'], 24 | render() { return '' }, 25 | }, 26 | }, 27 | }).$mount(); 28 | 29 | const spriteSheet = vm.$refs.spriteSheet; 30 | const xInject = vm.$refs.xInject; 31 | 32 | return {vm, spriteSheet, xInject}; 33 | }; 34 | 35 | it('renders', function () { 36 | const {vm, spriteSheet, xInject} = buildVm(); 37 | assert(spriteSheet); 38 | }); 39 | 40 | it('should have spriteSheet field', function () { 41 | const {vm, spriteSheet, xInject} = buildVm(); 42 | assert(spriteSheet.spriteSheet instanceof easeljs.SpriteSheet); 43 | }); 44 | 45 | it('should have images in the spriteSheet', function () { 46 | const {vm, spriteSheet, xInject} = buildVm(); 47 | assert(spriteSheet.spriteSheet._images); 48 | }); 49 | 50 | it('should have frames in the spriteSheet', function () { 51 | const {vm, spriteSheet, xInject} = buildVm(); 52 | assert(spriteSheet.spriteSheet._frameHeight === 32); 53 | assert(spriteSheet.spriteSheet._frameWidth === 32); 54 | }); 55 | 56 | it('should have animations in the spriteSheet', function () { 57 | const {vm, spriteSheet, xInject} = buildVm(); 58 | assert(spriteSheet.spriteSheet._animations.length > 0); 59 | }); 60 | 61 | it('should have framerate in the spriteSheet', function () { 62 | const {vm, spriteSheet, xInject} = buildVm(); 63 | assert(spriteSheet.spriteSheet.framerate === 30); 64 | }); 65 | 66 | it('should provide spriteSheet', function () { 67 | const {vm, spriteSheet, xInject} = buildVm(); 68 | assert(xInject.spriteSheet); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/EaselText.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Vue from 'vue'; 3 | import EaselText from '../src/components/EaselText.vue'; 4 | import easeljs from '../easeljs/easel.js'; 5 | import isADisplayObject from './includes/is-a-display-object.js'; 6 | import canCache from './includes/can-cache.js'; 7 | import canFilter from './includes/can-filter.js'; 8 | 9 | describe('EaselText', function () { 10 | 11 | describe('is a display object that', isADisplayObject(EaselText, 'text="O hai"')); 12 | 13 | // EaselText is also alignable, but it uses special alignment rules, so it 14 | // doesn't include the alignment tests. 15 | 16 | describe('is cacheable and', canCache(EaselText, {}, [ 17 | { 18 | name: 'color', 19 | value: 'black', 20 | changeTo: 'blue', 21 | shouldUpdateSameObject: true, 22 | }, 23 | { 24 | name: 'text', 25 | value: 'Oh, hi', 26 | changeTo: 'Ohai', 27 | shouldUpdateSameObject: true, 28 | }, 29 | { 30 | name: 'font', 31 | value: '12px "Times New Roman"', 32 | changeTo: '50px "Comic Sans"', 33 | shouldUpdateSameObject: true, 34 | }, 35 | ])); 36 | 37 | describe('can filter and', canFilter(EaselText, 'text="O hai"')); 38 | 39 | const buildVm = function () { 40 | const easel = { 41 | addChild(vueChild) { 42 | }, 43 | removeChild(vueChild) { 44 | }, 45 | }; 46 | 47 | const vm = new Vue({ 48 | template: ` 49 | 50 | 59 | 60 | 61 | `, 62 | provide() { 63 | return { 64 | easelParent: easel, 65 | easelCanvas: easel, 66 | }; 67 | }, 68 | data() { 69 | return { 70 | text: 'The Ran in Span Stays Manly On The Plan', 71 | showText: true, 72 | font: '20px Arial', 73 | color: 'black', 74 | align: ['left', 'top'], 75 | }; 76 | }, 77 | components: { 78 | 'easel-text': EaselText, 79 | }, 80 | }).$mount(); 81 | 82 | const text = vm.$refs.text; 83 | 84 | return {vm, text}; 85 | }; 86 | 87 | it('should exist', function () { 88 | const {vm, text} = buildVm(); 89 | assert(text); 90 | }); 91 | 92 | it('should have component field', function () { 93 | const {vm, text} = buildVm(); 94 | assert(text.component); 95 | }); 96 | 97 | it('should have the right text', function () { 98 | const {vm, text} = buildVm(); 99 | assert(vm.text === text.component.text, 'Wrong text: ' + text.component.text); 100 | }); 101 | 102 | it('should be able to change the text', function (done) { 103 | const {vm, text} = buildVm(); 104 | vm.text = Math.random(); 105 | Vue.nextTick() 106 | .then(() => { 107 | assert(vm.text === text.component.text, 'Wrong text in: ' + text.component.text); 108 | }) 109 | .then(done, done); 110 | }); 111 | 112 | it('should have the right font', function () { 113 | const {vm, text} = buildVm(); 114 | assert(vm.font === text.component.font, 'Wrong font: ' + text.component.font); 115 | }); 116 | 117 | it('should be able to change the font', function (done) { 118 | const {vm, text} = buildVm(); 119 | vm.font = Math.random(); 120 | Vue.nextTick() 121 | .then(() => { 122 | assert(vm.font === text.component.font, 'Wrong font in: ' + text.component.font); 123 | }) 124 | .then(done, done); 125 | }); 126 | 127 | it('should have the right color', function () { 128 | const {vm, text} = buildVm(); 129 | assert(vm.color === text.component.color, 'Wrong color: ' + text.component.color); 130 | }); 131 | 132 | it('should be able to change the color', function (done) { 133 | const {vm, text} = buildVm(); 134 | vm.color = 'grey'; 135 | Vue.nextTick() 136 | .then(() => { 137 | assert(vm.color === text.component.color, 'Wrong color in: ' + text.component.color); 138 | }) 139 | .then(done, done); 140 | }); 141 | 142 | it('should have the right align', function () { 143 | const {vm, text} = buildVm(); 144 | assert('left' === text.component.textAlign, 'Wrong textAlign: ' + text.component.textAlign); 145 | assert('top' === text.component.textBaseline, 'Wrong default textBaseline in: ' + text.component.textBaseline); 146 | }); 147 | 148 | it('should be able to change the align', function (done) { 149 | const {vm, text} = buildVm(); 150 | Vue.nextTick() 151 | .then(() => { 152 | vm.align = ['center', 'middle']; 153 | return Vue.nextTick(); 154 | }) 155 | .then(() => { 156 | assert('center' === text.component.textAlign, 'Wrong textAlign in: ' + text.component.textAlign); 157 | assert('middle' === text.component.textBaseline, 'Wrong default textBaseline in: ' + text.component.textBaseline); 158 | }) 159 | .then(done, done); 160 | }); 161 | 162 | it('should default align correctly', function (done) { 163 | const {vm, text} = buildVm(); 164 | Vue.nextTick() 165 | .then(() => { 166 | vm.align = ['', '']; 167 | return Vue.nextTick(); 168 | }) 169 | .then(() => { 170 | assert('top' === text.component.textBaseline, 'Wrong default textBaseline in: ' + text.component.textBaseline); 171 | assert('left' === text.component.textAlign, 'Wrong default textAlign in: ' + text.component.textAlign); 172 | }) 173 | .then(done, done); 174 | }); 175 | 176 | it('should normalize reversed array align prop', function (done) { 177 | const {vm, text} = buildVm(); 178 | Vue.nextTick() 179 | .then(() => { 180 | vm.align = ['bottom', 'right']; 181 | return Vue.nextTick(); 182 | }) 183 | .then(() => { 184 | assert('right' === text.component.textAlign, 'Wrong default textAlign in: ' + text.component.textAlign); 185 | assert('bottom' === text.component.textBaseline, 'Wrong default textBaseline in: ' + text.component.textBaseline); 186 | }) 187 | .then(done, done); 188 | }); 189 | 190 | it('should normalize reversed string align prop', function (done) { 191 | const {vm, text} = buildVm(); 192 | Vue.nextTick() 193 | .then(() => { 194 | vm.align = 'bottom-right'; 195 | return Vue.nextTick(); 196 | }) 197 | .then(() => { 198 | assert('right' === text.component.textAlign, 'Wrong default textAlign in: ' + text.component.textAlign); 199 | assert('bottom' === text.component.textBaseline, 'Wrong default textBaseline in: ' + text.component.textBaseline); 200 | }) 201 | .then(done, done); 202 | }); 203 | 204 | it('should convert center vertical to middle', function (done) { 205 | const {vm, text} = buildVm(); 206 | Vue.nextTick() 207 | .then(() => { 208 | vm.align = 'center-center'; 209 | return Vue.nextTick(); 210 | }) 211 | .then(() => { 212 | assert('center' === text.component.textAlign, 'Wrong default textAlign in: ' + text.component.textAlign); 213 | assert('middle' === text.component.textBaseline, 'Wrong default textBaseline in: ' + text.component.textBaseline); 214 | }) 215 | .then(done, done); 216 | }); 217 | 218 | 219 | it('should get cache bounds center-left', function (done) { 220 | const {vm, text} = buildVm(); 221 | vm.align = 'center-left'; 222 | Vue.nextTick() 223 | .then(() => text.getCacheBounds()) 224 | .then(({x, y, width, height}) => { 225 | assert(x === 0, `x is wrong: ${x}`); 226 | assert(Math.floor(y) === -8, `y is wrong: ${y}`); 227 | assert(Math.floor(width) === 381, `width is wrong: ${width}`); 228 | assert(Math.floor(height) === 19, `height is wrong: ${height}`); 229 | }) 230 | .then(done, done); 231 | }); 232 | 233 | it('should get cache bounds top-left', function (done) { 234 | const {vm, text} = buildVm(); 235 | vm.align = 'top-left'; 236 | Vue.nextTick() 237 | .then(() => text.getCacheBounds()) 238 | .then(({x, y, width, height}) => { 239 | assert(x === 0, `x is wrong: ${x}`); 240 | assert(y === 0, `y is wrong: ${y}`); 241 | assert(Math.floor(width) === 381, `width is wrong: ${width}`); 242 | assert(Math.floor(height) === 19, `height is wrong: ${height}`); 243 | }) 244 | .then(done, done); 245 | }); 246 | 247 | it('should get cache bounds bottom-right', function (done) { 248 | const {vm, text} = buildVm(); 249 | vm.align = 'bottom-right'; 250 | Vue.nextTick() 251 | .then(() => text.getCacheBounds()) 252 | .then(({x, y, width, height}) => { 253 | assert(Math.floor(x) === -382, `x is wrong: ${x}`); 254 | assert(Math.floor(y) === -20, `y is wrong: ${y}`); 255 | assert(Math.floor(width) === 381, `width is wrong: ${width}`); 256 | assert(Math.floor(height) === 19, `height is wrong: ${height}`); 257 | }) 258 | .then(done, done); 259 | }); 260 | 261 | }); 262 | -------------------------------------------------------------------------------- /test/Events.spec.js: -------------------------------------------------------------------------------- 1 | import Events from '../src/libs/Events.js'; 2 | import assert from 'assert'; 3 | const { 4 | deepStrictEqual: equal, 5 | notDeepStrictEqual: notEqual, 6 | } = assert; 7 | 8 | 9 | describe('Events', function () { 10 | 11 | it('exists', function () { 12 | new Events(); 13 | }); 14 | 15 | it('lets us know when fired', function () { 16 | const events = new Events(); 17 | let add = false; 18 | events.on('add', (param) => add = param); 19 | events.fire('add', '1234'); 20 | equal('1234', add); 21 | }); 22 | 23 | it('fires both cb even if one fails', function () { 24 | const events = new Events(); 25 | let ran; 26 | events.on('add', () => { throw new Error() }); 27 | events.on('add', () => ran = true); 28 | events.fire('add'); 29 | assert(ran); 30 | }); 31 | 32 | it('fires an error if an error happens', function () { 33 | const events = new Events({errorCode: 'error'}); 34 | let errored; 35 | events.on('add', () => { throw new Error() }); 36 | events.on('error', () => errored = true); 37 | events.fire('add'); 38 | assert(errored); 39 | }); 40 | 41 | it('does not fire an error if an error happens while firing an error', function () { 42 | const events = new Events({errorCode: 'error'}); 43 | events.on('error', () => { throw new Error() }); 44 | events.fire('error'); 45 | // no infinite recursion 46 | }); 47 | 48 | it('turn off', function () { 49 | const events = new Events(); 50 | let add = false; 51 | const cb = (param) => add = param; 52 | events.on('add', cb); 53 | events.fire('add', '1234'); 54 | equal('1234', add); 55 | events.off('add', cb); 56 | events.fire('add', 'nobody to listen'); 57 | equal('1234', add); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/FilterSet.spec.js: -------------------------------------------------------------------------------- 1 | import FilterSet from '../src/filters/FilterSet.js'; 2 | import assert from 'assert'; 3 | const {deepStrictEqual: equal} = assert; 4 | 5 | const randomName = () => 'Custom' + new String(Math.random()).substr(-8); 6 | 7 | describe('FilterSet', function () { 8 | 9 | it('registers and builds a class with applyFilter', function () { 10 | const name = randomName(); 11 | let caughtArguments; 12 | const filters = new FilterSet(); 13 | filters.register(name, class Custom { 14 | constructor(x) { 15 | this.x = x; 16 | } 17 | applyFilter(...args) { 18 | caughtArguments = args; 19 | return true; 20 | } 21 | }); 22 | const filter = filters.build([name, 'y']); 23 | assert(typeof filter.applyFilter === 'function'); 24 | assert(filter.applyFilter(1, 2, 3, 4, 5, 6, 7, 8)); 25 | equal([1, 2, 3, 4, 5, 6, 7, 8], caughtArguments); 26 | assert(filter.x === 'y'); 27 | }); 28 | 29 | it('registers and builds a class with adjustImageData', function () { 30 | const name = randomName(); 31 | let caughtArguments; 32 | const filters = new FilterSet(); 33 | filters.register(name, class Custom { 34 | constructor(x) { 35 | this.x = x; 36 | } 37 | adjustImageData(...args) { 38 | caughtArguments = args; 39 | return true; 40 | } 41 | }); 42 | const ctx = { 43 | getImageData(){ return 'image data'; }, 44 | putImageData(){ }, 45 | }; 46 | const filter = filters.build([name, 'y']); 47 | assert(typeof filter.applyFilter === 'function'); 48 | assert(filter.applyFilter(ctx, 2, 3, 4, 5, ctx, 7, 8)); 49 | equal(['image data'], caughtArguments); 50 | assert(filter.x === 'y'); 51 | }); 52 | 53 | it('registers and builds a class with adjustContext', function () { 54 | const name = randomName(); 55 | let caughtArguments; 56 | const filters = new FilterSet(); 57 | filters.register(name, class Custom { 58 | constructor(x) { 59 | this.x = x; 60 | } 61 | adjustContext(...args) { 62 | caughtArguments = args; 63 | return true; 64 | } 65 | }); 66 | const filter = filters.build([name, 'y']); 67 | assert(typeof filter.applyFilter === 'function'); 68 | assert(filter.applyFilter(1, 2, 3, 4, 5, 6, 7, 8)); 69 | equal([1, 2, 3, 4, 5, 6, 7, 8], caughtArguments); 70 | assert(filter.x === 'y'); 71 | }); 72 | 73 | it('rejects a class with none of those', function () { 74 | let caught; 75 | const filters = new FilterSet(); 76 | try { 77 | filters.register(randomName(), class Custom { 78 | }); 79 | } catch (e) { 80 | caught = e; 81 | } 82 | assert(caught); 83 | }); 84 | 85 | it('rejects a class with just _applyFilter', function () { 86 | let caught; 87 | const filters = new FilterSet(); 88 | try { 89 | filters.register(randomName(), class Custom { 90 | _applyFilter(){} 91 | }); 92 | } catch (e) { 93 | caught = e; 94 | } 95 | assert(caught); 96 | }); 97 | 98 | it('fails to build an unknown class', function () { 99 | let caught; 100 | const filters = new FilterSet(); 101 | try { 102 | filters.build(['NO_SUCH_NAME']); 103 | } catch (e) { 104 | caught = e; 105 | } 106 | assert(caught); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/PixelStrokeFilter.spec.js: -------------------------------------------------------------------------------- 1 | import PixelStrokeFilter from '../src/filters/PixelStrokeFilter.js'; 2 | import easeljs from '../easeljs/easel.js'; 3 | import assert from 'assert'; 4 | const {deepStrictEqual: equal} = assert; 5 | 6 | const stringify = (data) => { 7 | return data.map((n, i) => ('0' + n.toString(16)).substr(-2) + (i % 4 === 3 ? ' ' : '')).join(''); 8 | }; 9 | 10 | describe('PixelStrokeFilter', function () { 11 | 12 | it('instantiates', function () { 13 | new PixelStrokeFilter([], 1, {antiAlias: false}); 14 | }); 15 | 16 | it('draws a stroke around a tic-tac-toe center', function () { 17 | const imageData = { 18 | data: [ 19 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20 | 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 21 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22 | ], 23 | width: 3, 24 | height: 3, 25 | }; 26 | new PixelStrokeFilter([], 1, {antiAlias: false}).adjustImageData(imageData); 27 | equal( 28 | stringify([ 29 | 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 30 | 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 31 | 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 32 | ]), 33 | stringify(imageData.data) 34 | ); 35 | }); 36 | 37 | it('draws a stroke around a tic-tac-toe bottom-right corner', function () { 38 | const imageData = { 39 | data: [ 40 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42 | 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 43 | ], 44 | width: 3, 45 | height: 3, 46 | }; 47 | new PixelStrokeFilter([], 1, {antiAlias: false}).adjustImageData(imageData); 48 | equal( 49 | stringify([ 50 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51 | 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 52 | 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 53 | ]), 54 | stringify(imageData.data) 55 | ); 56 | }); 57 | 58 | it('draws a stroke around a tic-tac-toe bottom-right corner', function () { 59 | const imageData = { 60 | data: [ 61 | 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 62 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64 | ], 65 | width: 3, 66 | height: 3, 67 | }; 68 | new PixelStrokeFilter([], 1, {antiAlias: false}).adjustImageData(imageData); 69 | equal( 70 | stringify([ 71 | 255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 0, 72 | 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 73 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74 | ]), 75 | stringify(imageData.data) 76 | ); 77 | }); 78 | 79 | it('draws a colored stroke around a colored tic-tac-toe center', function () { 80 | const rand255 = () => Math.floor(Math.random() * 255); 81 | const sr = rand255(); 82 | const sg = rand255(); 83 | const sb = rand255(); 84 | const sa = rand255() || 255; 85 | const pr = rand255(); 86 | const pg = rand255(); 87 | const pb = rand255(); 88 | const pa = rand255() || 255; 89 | const imageData = { 90 | data: [ 91 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 92 | 0, 0, 0, 0, pr, pg, pb, pa, 0, 0, 0, 0, 93 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 94 | ], 95 | width: 3, 96 | height: 3, 97 | }; 98 | new PixelStrokeFilter([sr, sg, sb, sa], 1, {antiAlias: false}).adjustImageData(imageData); 99 | equal( 100 | stringify([ 101 | sr, sg, sb, sa, sr, sg, sb, sa, sr, sg, sb, sa, 102 | sr, sg, sb, sa, pr, pg, pb, pa, sr, sg, sb, sa, 103 | sr, sg, sb, sa, sr, sg, sb, sa, sr, sg, sb, sa, 104 | ]), 105 | stringify(imageData.data) 106 | ); 107 | }); 108 | 109 | it('calculates a size 1 circle', function () { 110 | const filter = new PixelStrokeFilter([0, 0, 0, 1], 1); 111 | const expected = { 112 | '1': {y: 1, minX: -1, maxX: 1, a: Math.sin(Math.PI / 4)}, 113 | '0': {y: 0, minX: -1, maxX: 1, a: 1}, 114 | '-1': {y: -1, minX: -1, maxX: 1, a: Math.sin(Math.PI / 4)}, 115 | }; 116 | equal(Object.keys(expected).length, filter.brush.length, JSON.stringify(filter.brush)); 117 | filter.brush.forEach(point => { 118 | equal(expected[point.y], point, JSON.stringify(filter.brush)); 119 | }); 120 | }); 121 | 122 | it('calculates a size 2 circle', function () { 123 | const filter = new PixelStrokeFilter([0, 0, 0, 1], 2); 124 | const expected = { 125 | '-2': {"y":-2, "minX":-2, "maxX":2, "a":0.41421356237309503}, 126 | '-1': {"y":-1, "minX":-2, "maxX":2, "a":0.8416407864998738}, 127 | '0': {"y":0, "minX":-2, "maxX":2, "a":1}, 128 | '1': {"y":1, "minX":-2, "maxX":2, "a":0.8416407864998738}, 129 | '2': {"y":2, "minX":-2, "maxX":2, "a":0.41421356237309503}, 130 | }; 131 | equal(Object.keys(expected).length, filter.brush.length, JSON.stringify(filter.brush)); 132 | filter.brush.forEach(point => { 133 | equal(expected[point.y], point, JSON.stringify(filter.brush)); 134 | }); 135 | }); 136 | 137 | it('calculates a size 20 circle', function () { 138 | const filter = new PixelStrokeFilter([0, 0, 0, 1], 20); 139 | const expected = { 140 | '-20': {"y":-20,"minX":0,"maxX":0,"a":1}, 141 | '-19': {"y":-19,"minX":-7,"maxX":7,"a":0.7}, 142 | '-18': {"y":-18,"minX":-9,"maxX":9,"a":0.9}, 143 | '-17': {"y":-17,"minX":-11,"maxX":11,"a":0.8}, 144 | '-16': {"y":-16,"minX":-12,"maxX":12,"a":1}, 145 | '-15': {"y":-15,"minX":-15,"maxX":15,"a":0.1}, 146 | '-14': {"y":-14,"minX":-15,"maxX":15,"a":0.6}, 147 | '-13': {"y":-13,"minX":-16,"maxX":16,"a":0.6}, 148 | '-12': {"y":-12,"minX":-16,"maxX":16,"a":1}, 149 | '-11': {"y":-11,"minX":-17,"maxX":17,"a":0.8}, 150 | '-10': {"y":-10,"minX":-18,"maxX":18,"a":0.6}, 151 | '-9': {"y":-9,"minX":-18,"maxX":18,"a":0.9}, 152 | '-8': {"y":-8,"minX":-19,"maxX":19,"a":0.5}, 153 | '-7': {"y":-7,"minX":-19,"maxX":19,"a":0.7}, 154 | '-6': {"y":-6,"minX":-20,"maxX":20,"a":0.5}, 155 | '-5': {"y":-5,"minX":-20,"maxX":20,"a":0.6}, 156 | '-4': {"y":-4,"minX":-20,"maxX":20,"a":0.8}, 157 | '-3': {"y":-3,"minX":-20,"maxX":20,"a":0.9}, 158 | '-2': {"y":-2,"minX":-20,"maxX":20,"a":0.9}, 159 | '-1': {"y":-1,"minX":-20,"maxX":20,"a":1}, 160 | '0': {"y":0,"minX":-20,"maxX":20,"a":1}, 161 | '1': {"y":1,"minX":-20,"maxX":20,"a":1}, 162 | '2': {"y":2,"minX":-20,"maxX":20,"a":0.9}, 163 | '3': {"y":3,"minX":-20,"maxX":20,"a":0.9}, 164 | '4': {"y":4,"minX":-20,"maxX":20,"a":0.8}, 165 | '5': {"y":5,"minX":-20,"maxX":20,"a":0.6}, 166 | '6': {"y":6,"minX":-20,"maxX":20,"a":0.5}, 167 | '7': {"y":7,"minX":-19,"maxX":19,"a":0.7}, 168 | '8': {"y":8,"minX":-19,"maxX":19,"a":0.5}, 169 | '9': {"y":9,"minX":-18,"maxX":18,"a":0.9}, 170 | '10': {"y":10,"minX":-18,"maxX":18,"a":0.6}, 171 | '11': {"y":11,"minX":-17,"maxX":17,"a":0.8}, 172 | '12': {"y":12,"minX":-16,"maxX":16,"a":1}, 173 | '13': {"y":13,"minX":-16,"maxX":16,"a":0.6}, 174 | '14': {"y":14,"minX":-15,"maxX":15,"a":0.6}, 175 | '15': {"y":15,"minX":-15,"maxX":15,"a":0.1}, 176 | '16': {"y":16,"minX":-12,"maxX":12,"a":1}, 177 | '17': {"y":17,"minX":-11,"maxX":11,"a":0.8}, 178 | '18': {"y":18,"minX":-9,"maxX":9,"a":0.9}, 179 | '19': {"y":19,"minX":-7,"maxX":7,"a":0.7}, 180 | '20': {"y":20,"minX":0,"maxX":0,"a":1}, 181 | }; 182 | equal(Object.keys(expected).length, filter.brush.length, JSON.stringify(filter.brush)); 183 | filter.brush.map(({y, minX, maxX, a}) => { return {y, minX, maxX, a: Math.round(a * 10) / 10}; }) 184 | .forEach(point => { 185 | equal(expected[point.y], point, JSON.stringify({expected: expected[point.y], actual: point})); 186 | }); 187 | }); 188 | 189 | it('expands a rectangle to include radius of brush', function () { 190 | const filter = new PixelStrokeFilter([0, 0, 0, 1], 20); 191 | const sourceRect = new easeljs.Rectangle(0, 0, 100, 100); 192 | const rect = filter.getBounds(sourceRect); 193 | equal( 194 | new easeljs.Rectangle(-40, -40, 180, 180), 195 | rect 196 | ); 197 | assert(sourceRect === rect); 198 | }); 199 | 200 | it('creates a rectangle to include radius of brush', function () { 201 | const filter = new PixelStrokeFilter([0, 0, 0, 1], 20); 202 | const rect = filter.getBounds(); 203 | equal( 204 | new easeljs.Rectangle(-40, -40, 80, 80), 205 | rect 206 | ); 207 | }); 208 | 209 | it('draws an anti-aliased stroke around a tic-tac-toe center', function () { 210 | const imageData = { 211 | data: [ 212 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 213 | 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 214 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 215 | ], 216 | width: 3, 217 | height: 3, 218 | }; 219 | new PixelStrokeFilter([], 1, {antiAlias: true}).adjustImageData(imageData); 220 | const cosine_of_45_degrees = .707; 221 | const alpha = Math.round(255 * cosine_of_45_degrees); 222 | equal( 223 | stringify([ 224 | 0, 0, 0, alpha, 0, 0, 0, 255, 0, 0, 0, alpha, 225 | 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 226 | 0, 0, 0, alpha, 0, 0, 0, 255, 0, 0, 0, alpha, 227 | ]), 228 | stringify(imageData.data) 229 | ); 230 | }); 231 | 232 | it('draws an anti-aliased stroke around a half-alpha tic-tac-toe center', function () { 233 | const imageData = { 234 | data: [ 235 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 236 | 0, 0, 0, 0, 255, 255, 255, 127, 0, 0, 0, 0, 237 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 238 | ], 239 | width: 3, 240 | height: 3, 241 | }; 242 | new PixelStrokeFilter([], 1, {antiAlias: true}).adjustImageData(imageData); 243 | const cosine_of_45_degrees = .707; 244 | const alpha = Math.round(255 * cosine_of_45_degrees); 245 | equal( 246 | stringify([ 247 | 0, 0, 0, alpha, 0, 0, 0, 255, 0, 0, 0, alpha, 248 | 0, 0, 0, 255, 255, 255, 255, 127, 0, 0, 0, 255, 249 | 0, 0, 0, alpha, 0, 0, 0, 255, 0, 0, 0, alpha, 250 | ]), 251 | stringify(imageData.data) 252 | ); 253 | }); 254 | }); 255 | -------------------------------------------------------------------------------- /test/PromiseParty.spec.js: -------------------------------------------------------------------------------- 1 | import PromiseParty from '../src/libs/PromiseParty.js'; 2 | import assert from 'assert'; 3 | const { 4 | deepStrictEqual: equal, 5 | notDeepStrictEqual: notEqual, 6 | } = assert; 7 | 8 | 9 | describe('PromiseParty', function () { 10 | 11 | it('exists', function () { 12 | new PromiseParty(); 13 | }); 14 | 15 | it('adds a Promise', function () { 16 | const party = new PromiseParty(); 17 | party.add(Promise.resolve()); 18 | }); 19 | 20 | it('does not add a not-Promise', function () { 21 | const party = new PromiseParty(); 22 | let error; 23 | try { 24 | party.add({}); 25 | } catch (e) { 26 | error = e; 27 | } 28 | assert(error); 29 | }); 30 | 31 | it('lets us know when a Promise is added', function () { 32 | const party = new PromiseParty(); 33 | let add = false; 34 | party.on('add', () => add = true); 35 | party.add(Promise.resolve()); 36 | assert(add); 37 | }); 38 | 39 | it('lets us know when a Promise completes', function (done) { 40 | const party = new PromiseParty(); 41 | let remove = false; 42 | party.on('remove', () => remove = true); 43 | let resolve; 44 | const promise = new Promise(r => resolve = r); 45 | party.add(promise); 46 | resolve(); 47 | promise 48 | .then(() => assert(remove)) 49 | .then(done, done); 50 | }); 51 | 52 | it('lets us know how many promises there are', function (done) { 53 | const party = new PromiseParty(); 54 | let lastAdded; 55 | let lastRemoved; 56 | let lastChanged; 57 | party.on('add', (count) => lastAdded = count); 58 | party.on('remove', (count) => lastRemoved = count); 59 | party.on('change', (count) => lastChanged = count); 60 | const promise1 = Promise.resolve(); 61 | let resolve2; 62 | const promise2 = new Promise(r => resolve2 = r); 63 | let reject3; 64 | const promise3 = new Promise((z, r) => reject3 = r); 65 | party.add(promise1); 66 | equal(1, lastAdded); 67 | equal(1, lastChanged); 68 | promise1 69 | .then(() => equal(0, lastRemoved)) 70 | .then(() => equal(0, lastChanged)) 71 | .then(() => { 72 | party.add(promise2); 73 | equal(1, lastAdded); // because of promise2 74 | equal(1, lastChanged); 75 | party.add(promise3); 76 | equal(2, lastAdded); // because of promise2 and promise3 77 | equal(2, lastChanged); 78 | equal(0, lastRemoved); // no change 79 | }) 80 | .then(() => resolve2()) 81 | .then(() => promise2) 82 | .then(() => { 83 | equal(2, lastAdded); // no change 84 | equal(1, lastRemoved); // because promise2 ended 85 | equal(1, lastChanged); 86 | }) 87 | .then(() => reject3()) 88 | .then(() => promise3) 89 | .then(() => { 90 | equal(2, lastAdded); // no change 91 | equal(0, lastRemoved); // because promise3 ended 92 | equal(0, lastChanged); 93 | }) 94 | .then(done, done); 95 | }); 96 | 97 | it('returns itself when adding and removing listeners', function () { 98 | const party = new PromiseParty(); 99 | const same = party.on('add', () => void(0)); 100 | assert(party === same); 101 | const sameAgain = party.off('not even a thing', () => void(0)); 102 | assert(party === sameAgain); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/easel-event-binder.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import EaselEventBinder from '../src/libs/easel-event-binder.js'; 3 | 4 | const eventTypes = ['added', 'click', 'dblclick', 'mousedown', 'mouseout', 'mouseover', 'pressmove', 'pressup', 'removed', 'rollout', 'rollover', 'tick', 'animationend', 'change']; 5 | const fauxParentListeners = {}; 6 | eventTypes.forEach(type => { 7 | fauxParentListeners[type] = function () {}; 8 | }); 9 | 10 | describe('easel-event-binder.js', function () { 11 | 12 | it('should bind all the things', function () { 13 | const got = {}; 14 | const vueComponent = { 15 | $options: { 16 | _parentListeners: fauxParentListeners, 17 | }, 18 | $emit(eventType, event) { 19 | got[eventType] = event; 20 | }, 21 | }; 22 | const easelComponent = { 23 | addEventListener(event, handler) { 24 | handler({type: event}); 25 | }, 26 | }; 27 | EaselEventBinder.bindEvents( 28 | vueComponent, 29 | easelComponent 30 | ); 31 | eventTypes.forEach(eventType => { 32 | assert(got[eventType]); 33 | }); 34 | }); 35 | 36 | it('should only bind dblclick', function () { 37 | const got = {}; 38 | const vueComponent = { 39 | $options: { 40 | _parentListeners: { 41 | dblclick() {}, 42 | NOT_A_REAL_EVENT_TYPE() {}, 43 | }, 44 | }, 45 | $emit(eventType, event) { 46 | got[eventType] = event; 47 | }, 48 | }; 49 | const easelComponent = { 50 | addEventListener(event, handler) { 51 | handler({type: event}); 52 | }, 53 | }; 54 | EaselEventBinder.bindEvents( 55 | vueComponent, 56 | easelComponent 57 | ); 58 | assert(Object.keys(got).length === 1, 'bound too many things'); 59 | assert(got.dblclick); 60 | }); 61 | 62 | it('should not bind', function () { 63 | const got = {}; 64 | const vueComponent = { 65 | $options: { 66 | }, 67 | $emit(eventType, event) { 68 | got[eventType] = event; 69 | }, 70 | }; 71 | const easelComponent = { 72 | addEventListener(event, handler) { 73 | handler({type: event}); 74 | }, 75 | }; 76 | EaselEventBinder.bindEvents( 77 | vueComponent, 78 | easelComponent 79 | ); 80 | assert(Object.keys(got).length === 0, 'bound too many things'); 81 | }); 82 | 83 | it('should augment an event via the canvas', function () { 84 | const event = {}; 85 | const easelCanvas = { 86 | augmentEvent(event) { 87 | event.changed = true; 88 | return event; 89 | }, 90 | }; 91 | const vueComponent = { 92 | easelCanvas, 93 | $options: { 94 | _parentListeners: { 95 | click() {}, 96 | }, 97 | }, 98 | $emit() {}, 99 | }; 100 | const easelComponent = { 101 | addEventListener(eventType, handler) { 102 | handler(event); 103 | }, 104 | }; 105 | EaselEventBinder.bindEvents(vueComponent, easelComponent); 106 | assert(event.changed); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/fixtures/EaselFake.js: -------------------------------------------------------------------------------- 1 | import EaselDisplayObject from '../../src/mixins/EaselDisplayObject.js'; 2 | import EaselCache from '../../src/mixins/EaselCache.js'; 3 | import easeljs from '../../easeljs/easel.js'; 4 | 5 | /** 6 | * A fake component that uses EaselDisplayObject. 7 | * It uses a generic Shape internally. 8 | * It has the size of a 32x48 rectangle. 9 | */ 10 | export default { 11 | template: '', 12 | mixins: [EaselDisplayObject, EaselCache], 13 | mounted() { 14 | this.component = new easeljs.Shape(); 15 | }, 16 | methods: { 17 | getAlignDimensions() { 18 | return Promise.resolve({width: 32, height: 48}); 19 | }, 20 | getCacheBounds() { 21 | return Promise.resolve({x: -10, y: -20, width: 30, height: 40}); 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /test/fixtures/Set.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cheap implementation of modern JS's Set class 3 | */ 4 | export default class Set { 5 | 6 | constructor(array = []) { 7 | this.array = [...array]; 8 | this.size = array.length; 9 | } 10 | 11 | has(element) { 12 | return this.array.indexOf(element) >= 0; 13 | } 14 | 15 | add(element) { 16 | if (!this.has(element)) { 17 | this.array.push(element); 18 | this.size = this.array.length; 19 | } 20 | } 21 | 22 | delete(element) { 23 | if (this.has(element)) { 24 | this.array.splice(this.array.indexOf(element), 1); 25 | this.size = this.array.length; 26 | } 27 | } 28 | 29 | // These should be easy to implement, but I haven't checked them against a 30 | // real implementation: 31 | 32 | // forEach(cb) { 33 | // this.array.forEach(cb); 34 | // } 35 | 36 | // keys() { 37 | // return this.values(); 38 | // } 39 | 40 | // values() { 41 | // return [...this.array]; 42 | // } 43 | }; 44 | -------------------------------------------------------------------------------- /test/images/gulfstream_park.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dankuck/vue-easeljs/ffcbcb667a0029c91de48e8a27fe4bdec7a8df4b/test/images/gulfstream_park.jpg -------------------------------------------------------------------------------- /test/images/lastguardian-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dankuck/vue-easeljs/ffcbcb667a0029c91de48e8a27fe4bdec7a8df4b/test/images/lastguardian-all.png -------------------------------------------------------------------------------- /test/includes/can-cache.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import assert from 'assert'; 3 | 4 | const wait = function (component, count = 1) { 5 | let promise = Promise.all([ 6 | component.getCacheBounds(), 7 | Vue.nextTick(), 8 | ]); 9 | if (count > 1) { 10 | return promise.then(() => wait(component, count - 1)); 11 | } else { 12 | return promise; 13 | } 14 | }; 15 | 16 | const parentPropChangers = [ 17 | { 18 | name: 'x', 19 | value: 0, 20 | changeTo: 1, 21 | shouldUpdateSameObject: false, 22 | }, 23 | { 24 | name: 'y', 25 | value: 0, 26 | changeTo: 2, 27 | shouldUpdateSameObject: false, 28 | }, 29 | { 30 | name: 'flip', 31 | value: '', 32 | changeTo: 'horizontal', 33 | shouldUpdateSameObject: false, 34 | }, 35 | { 36 | name: 'rotation', 37 | value: '', 38 | changeTo: '90', 39 | shouldUpdateSameObject: false, 40 | }, 41 | { 42 | name: 'scale', 43 | value: 1, 44 | changeTo: 2, 45 | shouldUpdateSameObject: true, 46 | }, 47 | { 48 | name: 'alpha', 49 | value: 1, 50 | changeTo: .5, 51 | shouldUpdateSameObject: false, 52 | }, 53 | { 54 | name: 'shadow', 55 | value: ['red', 5, 6, .5], 56 | changeTo: ['blue', 5, 6, .5], 57 | shouldUpdateSameObject: false, 58 | }, 59 | // `align` could technically be on this list, but it's not guaranteed to be 60 | // on all EaselCache components 61 | ]; 62 | 63 | export default function (implementor, provide = {}, propChangers = []) { 64 | return function () { 65 | 66 | const allPropChangers = propChangers 67 | .concat(parentPropChangers) 68 | 69 | const buildVm = function () { 70 | /** 71 | * A fake easel object 72 | */ 73 | const easel = { 74 | addChild() { 75 | }, 76 | removeChild() { 77 | }, 78 | cacheNeedsUpdate: false, 79 | createCanvas(cb) { 80 | return cb(); 81 | }, 82 | }; 83 | 84 | const container = new Vue({ 85 | provide() { 86 | return { 87 | easelParent: easel, 88 | easelCanvas: easel, 89 | }; 90 | }, 91 | data() { 92 | return { 93 | addChild() { 94 | }, 95 | removeChild() { 96 | }, 97 | cacheNeedsUpdate: false, 98 | createCanvas(cb) { 99 | return cb(); 100 | }, 101 | scale: 1, 102 | }; 103 | }, 104 | }); 105 | 106 | const props = allPropChangers 107 | .map(changer => changer.name) 108 | .map(name => `:${name}="${name}"`) 109 | .join("\n"); 110 | 111 | const vm = new Vue({ 112 | template: ` 113 | 114 | 118 | 119 | 120 | `, 121 | provide() { 122 | provide.easelParent = container; 123 | provide.easelCanvas = easel; 124 | return provide; 125 | }, 126 | data() { 127 | const data = { 128 | cache: true, 129 | }; 130 | return allPropChangers 131 | .reduce((acc, changer) => { 132 | acc[changer.name] = changer.value; 133 | return acc; 134 | }, data); 135 | }, 136 | components: { 137 | implementor, 138 | }, 139 | methods: { 140 | }, 141 | }).$mount(); 142 | 143 | const fake = vm.$refs.fake; 144 | 145 | return {fake, vm, easel, container}; 146 | }; 147 | 148 | it('can cache on init', function (done) { 149 | const {vm, fake} = buildVm(); 150 | assert(fake.cache === true); 151 | assert(fake.component.cacheCanvas === null); 152 | wait(fake) 153 | .then(() => { 154 | assert(fake.component.cacheCanvas !== null, 'Did not create cache'); 155 | }) 156 | .then(done, done); 157 | }); 158 | 159 | it('can clear cache', function (done) { 160 | const {vm, fake} = buildVm(); 161 | assert(fake.cache === true); 162 | wait(fake) 163 | .then(() => { 164 | assert(fake.component.cacheCanvas !== null, 'Did not create cache'); 165 | vm.cache = false; 166 | return wait(fake, 4); 167 | }) 168 | .then(() => { 169 | assert(fake.component.cacheCanvas === null, 'Did not destroy cache'); 170 | }) 171 | .then(done, done); 172 | }); 173 | 174 | it('can clear and recreate cache', function (done) { 175 | const {vm, fake} = buildVm(); 176 | assert(fake.cache === true); 177 | wait(fake) 178 | .then(() => { 179 | assert(fake.component.cacheCanvas !== null, 'Did not create cache'); 180 | vm.cache = false; 181 | return wait(fake); 182 | }) 183 | .then(() => { 184 | assert(fake.component.cacheCanvas === null, 'Did not destroy cache'); 185 | vm.cache = true; 186 | return wait(fake, 2); 187 | }) 188 | .then(() => { 189 | assert(fake.component.cacheCanvas !== null, 'Did not re-create cache'); 190 | }) 191 | .then(done, done); 192 | }); 193 | 194 | // Search catchers: 195 | // ... should YES update cache when ... 196 | // ... should NOT update cache when ... 197 | allPropChangers 198 | .forEach(({name, changeTo, shouldUpdateSameObject}) => { 199 | it(`should ${shouldUpdateSameObject ? 'YES' : 'NOT'} update cache when ${name} changes`, function (done) { 200 | const {vm, fake} = buildVm(); 201 | assert(fake.cache === true); 202 | let bitmapCache, cacheID; 203 | wait(fake) 204 | .then(() => { 205 | assert(fake.component.cacheCanvas !== null, 'Did not create cache'); 206 | bitmapCache = fake.component.bitmapCache; 207 | cacheID = bitmapCache && bitmapCache.cacheID; 208 | vm[name] = changeTo; 209 | return wait(fake); 210 | }) 211 | .then(() => { 212 | // Check the bitmapCache object and the cacheID 213 | // because the cache may have updated by replacing 214 | // the whole object or it may have just updated and 215 | // incremented its ID. 216 | const updated = ( 217 | bitmapCache !== fake.component.bitmapCache 218 | || cacheID !== bitmapCache.cacheID 219 | ); 220 | assert(updated === shouldUpdateSameObject, `${name} did ${updated ? 'YES' : 'NOT'} cause an update: ${fake[name]}`); 221 | }) 222 | .then(done, done); 223 | }); 224 | }); 225 | 226 | // Search catchers: 227 | // ... should YES update easel.cacheNeedsUpdate when ... 228 | // ... should NOT update easel.cacheNeedsUpdate when ... 229 | allPropChangers 230 | .forEach(({name, changeTo}) => { 231 | it(`should update easel.cacheNeedsUpdate when ${name} changes`, function (done) { 232 | const {vm, fake, easel, container} = buildVm(); 233 | // Works whether or not cache is on for this component 234 | vm.cache = Math.random() > .5 ? true : false; 235 | // Let the component catch up with `cache` change 236 | wait(fake) 237 | .then(() => { 238 | easel.cacheNeedsUpdate = false; 239 | vm[name] = changeTo; 240 | return wait(fake); 241 | }) 242 | .then(() => { 243 | assert(container.cacheNeedsUpdate === true, `${name} did NOT cause an update: ${fake[name]}`); 244 | }) 245 | .then(done, done); 246 | }); 247 | }); 248 | 249 | it('should update on change event', function (done) { 250 | const {vm, fake, easel, container} = buildVm(); 251 | assert(fake.cache === true); 252 | let bitmapCache, cacheID; 253 | wait(fake, 2) // EaselBitmap needs an extra tick 254 | .then(() => { 255 | assert(fake.component.cacheCanvas !== null, 'Did not create cache'); 256 | bitmapCache = fake.component.bitmapCache; 257 | cacheID = bitmapCache && bitmapCache.cacheID; 258 | easel.cacheNeedsUpdate = false; 259 | fake.component.dispatchEvent('change') 260 | return wait(fake); 261 | }) 262 | .then(() => { 263 | // Check the bitmapCache object and the cacheID 264 | // because the cache may have updated by replacing 265 | // the whole object or it may have just updated and 266 | // incremented its ID. 267 | const updated = ( 268 | bitmapCache !== fake.component.bitmapCache 269 | || cacheID !== bitmapCache.cacheID 270 | ); 271 | assert(updated, `Event 'change' did not cause an update`); 272 | assert(container.cacheNeedsUpdate === true, `Event 'change' did NOT cause an update to container.cacheNeedsUpdate`); 273 | }) 274 | .then(done, done); 275 | }); 276 | 277 | it('should update cache on window resize', function (done) { 278 | const {vm, fake, easel, container} = buildVm(); 279 | let bitmapCache, cacheID; 280 | wait(fake, 2) // EaselBitmap needs an extra tick 281 | .then(() => { 282 | assert(fake.component.cacheCanvas !== null, 'Did not create cache'); 283 | bitmapCache = fake.component.bitmapCache; 284 | cacheID = bitmapCache && bitmapCache.cacheID; 285 | window.dispatchEvent(new Event('resize')); 286 | return wait(fake); 287 | }) 288 | .then(() => { 289 | // Check the bitmapCache object and the cacheID 290 | // because the cache may have updated by replacing 291 | // the whole object or it may have just updated and 292 | // incremented its ID. 293 | const updated = ( 294 | bitmapCache !== fake.component.bitmapCache 295 | || cacheID !== bitmapCache.cacheID 296 | ); 297 | assert(updated, `Window resize did not cause an update`); 298 | assert(container.cacheNeedsUpdate === true, `Window resize did NOT cause an update to container.cacheNeedsUpdate`); 299 | }) 300 | .then(done, done); 301 | }); 302 | 303 | it('should update cache size on update', function (done) { 304 | const {vm, fake} = buildVm(); 305 | assert(fake.cache === true); 306 | let width, height; 307 | wait(fake, 2) 308 | .then(() => { 309 | assert(fake.component.cacheCanvas !== null, 'Did not create cache'); 310 | width = fake.component.cacheCanvas.width; 311 | height = fake.component.cacheCanvas.height; 312 | vm.scale = 10; 313 | return wait(fake, 2); 314 | }) 315 | .then(() => { 316 | assert(fake.component.cacheCanvas.width > (width - 1) * 10); 317 | assert(fake.component.cacheCanvas.height > (height - 1) * 10); 318 | assert(fake.component.cacheCanvas.width < (width + 1) * 10); 319 | assert(fake.component.cacheCanvas.height < (height + 1) * 10); 320 | }) 321 | .then(done, done); 322 | }); 323 | 324 | it('runs beforeCache on cache init', function (done) { 325 | const {vm, fake} = buildVm(); 326 | let ran = false; 327 | fake.beforeCache(() => ran = true); 328 | assert(fake.cache === true); 329 | assert(fake.component.cacheCanvas === null); 330 | wait(fake) 331 | .then(() => { 332 | assert(ran); 333 | }) 334 | .then(done, done); 335 | }); 336 | 337 | it('runs beforeCache on cache update', function (done) { 338 | const {vm, fake} = buildVm(); 339 | let ran = false; 340 | assert(fake.cache === true); 341 | assert(fake.component.cacheCanvas === null); 342 | wait(fake, 2) 343 | .then(() => { 344 | assert(!ran); 345 | fake.beforeCache(() => ran = true); 346 | fake.cacheNeedsUpdate = true; 347 | return wait(fake, 2); 348 | }) 349 | .then(() => { 350 | assert(ran); 351 | }) 352 | .then(done, done); 353 | }); 354 | 355 | it('caches based on cacheWhen()', function (done) { 356 | const {vm, fake} = buildVm(); 357 | wait(fake, 2) 358 | .then(() => { 359 | vm.cache = false; 360 | return wait(fake, 2); 361 | }) 362 | .then(() => { 363 | assert(fake.component.cacheCanvas === null, 'still cached'); 364 | fake.cacheWhen(() => true); 365 | return wait(fake, 2); 366 | }) 367 | .then(() => { 368 | assert(fake.component.cacheCanvas !== null, 'Did not create cache'); 369 | }) 370 | .then(done, done); 371 | }); 372 | 373 | it('uses the right scale to cache', function (done) { 374 | const {vm, fake, easel, container} = buildVm(); 375 | container.scale = 2; 376 | vm.scale = 2; 377 | Vue.nextTick() 378 | .then(() => { 379 | assert(fake.cacheScale === 2 * 2, 'cacheScale: ' + fake.cacheScale); 380 | }) 381 | .then(done, done); 382 | }); 383 | 384 | it('has scale in its updatesEaselCache', function () { 385 | assert(implementor.updatesEaselCache.indexOf('scale') >= 0, implementor.updatesEaselCache); 386 | }); 387 | }; 388 | }; 389 | -------------------------------------------------------------------------------- /test/includes/can-filter.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import assert from 'assert'; 3 | import easeljs from '../../easeljs/easel.js'; 4 | import VueEaseljs from '../../src/index.js'; 5 | 6 | const wait = function (component, count = 1) { 7 | let promise = Promise.all([ 8 | component.getCacheBounds(), 9 | Vue.nextTick(), 10 | ]); 11 | if (count > 1) { 12 | return promise.then(() => wait(component, count - 1)); 13 | } else { 14 | return promise; 15 | } 16 | }; 17 | 18 | export default function (implementor, extra_attributes = '', provide = {}) { 19 | return function () { 20 | 21 | const buildVm = function () { 22 | /** 23 | * A fake easel object 24 | */ 25 | const easel = { 26 | addChild() { 27 | }, 28 | removeChild() { 29 | }, 30 | createCanvas(cb) { 31 | return cb(); 32 | }, 33 | }; 34 | 35 | const vm = new Vue({ 36 | template: ` 37 | 38 | 44 | 45 | 46 | `, 47 | provide() { 48 | provide.easelParent = easel; 49 | provide.easelCanvas = easel; 50 | return provide; 51 | }, 52 | data() { 53 | return { 54 | showFake: true, 55 | cache: false, 56 | filters: null, 57 | }; 58 | }, 59 | components: { 60 | implementor, 61 | }, 62 | methods: { 63 | }, 64 | }).$mount(); 65 | 66 | const fake = vm.$refs.fake; 67 | 68 | return {fake, vm, easel}; 69 | }; 70 | 71 | it('should exist', function () { 72 | const {vm, fake} = buildVm(); 73 | assert(fake); 74 | }); 75 | 76 | it('should have $el', function () { 77 | const {vm, fake} = buildVm(); 78 | assert(fake.$el); 79 | }); 80 | 81 | it('should have component field', function () { 82 | const {vm, fake} = buildVm(); 83 | assert(fake.component); 84 | }); 85 | 86 | it('should have a filter', function (done) { 87 | const {vm, fake} = buildVm(); 88 | vm.filters = [['BlurFilter', 5, 5, 1]]; 89 | vm.cache = true; 90 | wait(fake, 2) 91 | .then(() => { 92 | assert(fake.component.cacheCanvas !== null, 'no cache'); 93 | assert(fake.component.filters); 94 | assert(fake.component.filters.length === 1); 95 | assert(fake.component.filters[0] instanceof easeljs.BlurFilter); 96 | }) 97 | .then(done, done); 98 | }); 99 | 100 | it('should cache when filtering even when caching is not explicit', function (done) { 101 | const {vm, fake} = buildVm(); 102 | assert(fake.component.cacheCanvas === null); 103 | wait(fake, 2) 104 | .then(() => { 105 | vm.cache = false; 106 | return wait(fake, 2); 107 | }) 108 | .then(() => { 109 | assert(fake.component.cacheCanvas === null, 'still cached'); 110 | vm.filters = [['BlurFilter', 5, 5, 1]]; 111 | return wait(fake, 2); 112 | }) 113 | .then(() => { 114 | assert(fake.component.cacheCanvas !== null, 'no cache'); 115 | }) 116 | .then(done, done); 117 | }); 118 | 119 | it('should add and remove filters', function (done) { 120 | const {vm, fake} = buildVm(); 121 | assert(fake.component.cacheCanvas === null); 122 | wait(fake, 2) 123 | .then(() => { 124 | vm.cache = false; 125 | return wait(fake, 2); 126 | }) 127 | .then(() => { 128 | assert(fake.component.cacheCanvas === null, 'still cached'); 129 | vm.filters = [['BlurFilter', 5, 5, 1]]; 130 | return wait(fake, 2); 131 | }) 132 | .then(() => { 133 | assert(fake.component.cacheCanvas !== null, 'no cache'); 134 | vm.filters = null; 135 | return wait(fake, 2); 136 | }) 137 | .then(() => { 138 | assert(fake.component.cacheCanvas === null, 'still cached 2'); 139 | vm.cache = true; 140 | return wait(fake, 2); 141 | }) 142 | .then(() => { 143 | assert(!fake.component.filters, 'still has filters'); 144 | assert(fake.component.cacheCanvas !== null, 'no cache 2'); 145 | }) 146 | .then(done, done); 147 | }); 148 | 149 | it('should fail on bad filters', function (done) { 150 | const {vm, fake} = buildVm(); 151 | let caughtError; 152 | const originalError = console.error; 153 | console.error = (msg) => caughtError = msg; 154 | vm.filters = [['NO_SUCH_FILTER', 'whatever param']]; 155 | wait(fake, 2) 156 | .then(() => { 157 | assert(!fake.component.filters || fake.component.filters.length === 0); 158 | assert(caughtError); 159 | }) 160 | .finally(() => console.error = originalError) 161 | .then(done, done); 162 | }); 163 | 164 | [ 165 | ['BlurFilter', 5, 5, 1], 166 | ['ColorFilter', 0, 0, 0, 1, 0, 0, 255, 0], 167 | ['ColorMatrixFilter', 1, 1, 1, 1], 168 | ].forEach((filter) => { 169 | it(`should use filter ${filter[0]}`, function (done) { 170 | const {vm, fake} = buildVm(); 171 | vm.filters = [filter]; 172 | vm.cache = true; 173 | wait(fake, 2) 174 | .then(() => { 175 | assert(fake.component.cacheCanvas !== null, 'no cache'); 176 | assert(fake.component.filters); 177 | assert(fake.component.filters.length === 1); 178 | }) 179 | .then(done, done); 180 | }); 181 | }); 182 | 183 | it('should use a custom simple filter class', function (done) { 184 | const {vm, fake} = buildVm(); 185 | const name = 'Custom' + new String(Math.random()).substr(-8); 186 | const Custom = class Custom { 187 | adjustImageData(){} 188 | }; 189 | VueEaseljs.registerFilter(name, Custom); 190 | vm.filters = [[name]]; 191 | wait(fake, 2) 192 | .then(() => { 193 | assert(fake.component.cacheCanvas !== null, 'no cache'); 194 | assert(fake.component.filters); 195 | assert(fake.component.filters.length === 1); 196 | }) 197 | .then(done, done); 198 | }); 199 | }; 200 | }; 201 | -------------------------------------------------------------------------------- /test/includes/does-events.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Vue from 'vue'; 3 | import {eventTypes} from '../../src/libs/easel-event-binder.js'; 4 | 5 | assert(eventTypes && eventTypes.length > 0, 'easel-event-binder.js did not return a good eventTypes array'); 6 | 7 | export default function (implementor, extra_attributes = '', provide = {}) { 8 | 9 | return function () { 10 | 11 | const buildVm = function () { 12 | const easel = { 13 | addChild(vueChild) { 14 | }, 15 | removeChild(vueChild) { 16 | }, 17 | }; 18 | 19 | const eventHandlerCode = eventTypes.map(type => `@${type}="logEvent"`).join(' '); 20 | 21 | const vm = new Vue({ 22 | template: ` 23 | 24 | 28 | 29 | 30 | `, 31 | provide() { 32 | provide.easelParent = easel; 33 | provide.easelCanvas = easel; 34 | return provide; 35 | }, 36 | data() { 37 | return { 38 | eventLog: [], 39 | }; 40 | }, 41 | components: { 42 | implementor, 43 | }, 44 | methods: { 45 | logEvent(event) { 46 | this.eventLog.push(event); 47 | }, 48 | clearEventLog() { 49 | this.eventLog = []; 50 | }, 51 | }, 52 | }).$mount(); 53 | 54 | const fake = vm.$refs.fake; 55 | 56 | return {vm, fake}; 57 | }; 58 | 59 | it('should exist', function () { 60 | const {fake} = buildVm(); 61 | assert(fake); 62 | }); 63 | 64 | eventTypes.forEach(type => { 65 | it(`emits ${type} event`, function (done) { 66 | const {vm, fake} = buildVm(); 67 | Vue.nextTick() 68 | .then(() => { 69 | vm.clearEventLog(); 70 | fake.component.dispatchEvent(type); 71 | assert(vm.eventLog.length === 1, `wrong number of events ${vm.eventLog.length}`); 72 | }) 73 | .then(done, done) 74 | }); 75 | }); 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /test/includes/is-a-display-object.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Vue from 'vue'; 3 | import doesEvents from './does-events.js'; 4 | import easeljs from '../../easeljs/easel.js'; 5 | 6 | /** 7 | * Returns a function to be used with `describe` for any component that is an 8 | * EaselDisplayObject. 9 | * 10 | * Like this: 11 | * `describe('is a display object that', isADisplayObject(MyComponent)` 12 | * @param VueComponent implementor 13 | * @return function 14 | */ 15 | export default function (implementor, extra_attributes = '', provide = {}) { 16 | 17 | return function () { 18 | 19 | describe('does events and', doesEvents(implementor, extra_attributes, provide)); 20 | 21 | 22 | const buildVm = function () { 23 | /** 24 | * A fake easel object. It allows adding and removing a child and has extra 25 | * methods to tell whether the object was added and removed. 26 | */ 27 | const easel = { 28 | gotChild(vueChild) { 29 | return vueChild.added; 30 | }, 31 | lostChild(vueChild) { 32 | return vueChild.removed; 33 | }, 34 | addChild(vueChild) { 35 | vueChild.added = true; 36 | }, 37 | removeChild(vueChild) { 38 | vueChild.removed = true; 39 | }, 40 | }; 41 | 42 | const vm = new Vue({ 43 | template: ` 44 | 45 | 60 | 61 | 62 | `, 63 | provide() { 64 | provide.easelParent = easel; 65 | provide.easelCanvas = easel; 66 | return provide; 67 | }, 68 | data() { 69 | return { 70 | x: 1, 71 | y: 2, 72 | eventLog: [], 73 | showFake: true, 74 | flip: '', 75 | rotation: null, 76 | scale: 1, 77 | alpha: null, 78 | shadow: null, 79 | hAlign: 'left', 80 | vAlign: 'top', 81 | cursor: null, 82 | visible: null, 83 | name: null, 84 | }; 85 | }, 86 | components: { 87 | implementor, 88 | }, 89 | methods: { 90 | logEvent(event) { 91 | this.eventLog.push(event); 92 | }, 93 | clearEventLog() { 94 | this.eventLog = []; 95 | }, 96 | }, 97 | }).$mount(); 98 | 99 | const fake = vm.$refs.fake; 100 | 101 | return {fake, vm, easel}; 102 | }; 103 | 104 | it('should exist', function () { 105 | const {fake, vm, easel} = buildVm(); 106 | assert(fake); 107 | }); 108 | 109 | it('should have same easel', function () { 110 | const {fake, vm, easel} = buildVm(); 111 | assert(fake.easelParent === easel); 112 | }); 113 | 114 | it('should have component field', function () { 115 | const {fake, vm, easel} = buildVm(); 116 | assert(fake.component); 117 | }); 118 | 119 | it('should have been added', function (done) { 120 | const {fake, vm, easel} = buildVm(); 121 | Vue.nextTick() 122 | .then(() => { 123 | assert(easel.gotChild(fake)); 124 | }) 125 | .then(done, done); 126 | }); 127 | 128 | it('should go away when gone', function (done) { 129 | const {fake, vm, easel} = buildVm(); 130 | vm.showFake = false; 131 | Vue.nextTick() 132 | .then(() => { 133 | assert(easel.lostChild(fake)); 134 | vm.showFake = true; 135 | return Vue.nextTick(); 136 | }) 137 | .then(done, done); 138 | }); 139 | 140 | it('should have x and y', function (done) { 141 | const {fake, vm, easel} = buildVm(); 142 | Vue.nextTick() 143 | .then(() => { 144 | assert(fake.component.x === 1); 145 | assert(fake.component.y === 2); 146 | }) 147 | .then(done, done); 148 | }); 149 | 150 | it('should change x and y', function (done) { 151 | const {fake, vm, easel} = buildVm(); 152 | vm.x = 3; 153 | vm.y = 4; 154 | Vue.nextTick() 155 | .then(() => { 156 | assert(fake.component.x === 3); 157 | assert(fake.component.y === 4); 158 | }) 159 | .then(done, done); 160 | }); 161 | 162 | it('should not flip', function (done) { 163 | const {fake, vm, easel} = buildVm(); 164 | vm.flip = ''; 165 | Vue.nextTick() 166 | .then(() => { 167 | assert(fake.component.scaleX === 1); 168 | assert(fake.component.scaleY === 1); 169 | }) 170 | .then(done, done); 171 | }); 172 | 173 | it('should flip horizontal', function (done) { 174 | const {fake, vm, easel} = buildVm(); 175 | vm.flip = 'horizontal'; 176 | Vue.nextTick() 177 | .then(() => { 178 | assert(fake.component.scaleX === -1); 179 | assert(fake.component.scaleY === 1); 180 | }) 181 | .then(done, done); 182 | }); 183 | 184 | it('should flip vertical', function (done) { 185 | const {fake, vm, easel} = buildVm(); 186 | vm.flip = 'vertical'; 187 | Vue.nextTick() 188 | .then(() => { 189 | assert(fake.component.scaleX === 1); 190 | assert(fake.component.scaleY === -1); 191 | }) 192 | .then(done, done); 193 | }); 194 | 195 | it('should flip both', function (done) { 196 | const {fake, vm, easel} = buildVm(); 197 | vm.flip = 'both'; 198 | Vue.nextTick() 199 | .then(() => { 200 | assert(fake.component.scaleX === -1); 201 | assert(fake.component.scaleY === -1); 202 | }) 203 | .then(done, done); 204 | }); 205 | 206 | it('should not scale', function (done) { 207 | const {fake, vm, easel} = buildVm(); 208 | vm.flip = ''; 209 | Vue.nextTick() 210 | .then(() => { 211 | assert(fake.component.scaleX === 1); 212 | assert(fake.component.scaleY === 1); 213 | }) 214 | .then(done, done); 215 | }); 216 | 217 | it('should scale to double', function (done) { 218 | const {fake, vm, easel} = buildVm(); 219 | vm.scale = 2; 220 | Vue.nextTick() 221 | .then(() => { 222 | assert(fake.component.scaleX === 2); 223 | assert(fake.component.scaleY === 2); 224 | }) 225 | .then(done, done); 226 | }); 227 | 228 | it('should scale and flip', function (done) { 229 | const {fake, vm, easel} = buildVm(); 230 | vm.scale = 2; 231 | vm.flip = "both"; 232 | Vue.nextTick() 233 | .then(() => { 234 | assert(fake.component.scaleX === -2); 235 | assert(fake.component.scaleY === -2); 236 | }) 237 | .then(done, done); 238 | }); 239 | 240 | it('should be 100% opaque', function () { 241 | const {fake, vm, easel} = buildVm(); 242 | assert(fake.component.alpha === 1, "Wrong alpha: " + fake.component.alpha); 243 | }); 244 | 245 | it('should become 50% opaque', function (done) { 246 | const {fake, vm, easel} = buildVm(); 247 | vm.alpha = .5; 248 | Vue.nextTick() 249 | .then(() => { 250 | assert(fake.component.alpha === .5, "Wrong alpha: " + fake.component.alpha); 251 | }) 252 | .then(done, done); 253 | }); 254 | 255 | it('should have no shadow', function () { 256 | const {fake, vm, easel} = buildVm(); 257 | assert(fake.component.shadow === null); 258 | }); 259 | 260 | it('should have shadow', function (done) { 261 | const {fake, vm, easel} = buildVm(); 262 | vm.shadow = ["black", 5, 7, 10]; 263 | Vue.nextTick() 264 | .then(() => { 265 | assert(fake.component, 'missing component'); 266 | assert(fake.component.shadow, 'missing shadow'); 267 | assert(fake.component.shadow.color, 'missing color'); 268 | assert(fake.component.shadow.color === 'black', 'Shadow color: ' + fake.component.shadow.color); 269 | assert(fake.component.shadow.offsetX === 5, 'Shadow offsetX: ' + fake.component.shadow.offsetX); 270 | assert(fake.component.shadow.offsetY === 7, 'Shadow offsetY: ' + fake.component.shadow.offsetY); 271 | assert(fake.component.shadow.blur === 10, 'Shadow blur: ' + fake.component.shadow.blur); 272 | }) 273 | .then(done, done); 274 | }); 275 | 276 | it('should have no shadow again', function (done) { 277 | const {fake, vm, easel} = buildVm(); 278 | vm.shadow = ["black", 5, 7, 10]; 279 | Vue.nextTick() 280 | .then(() => { 281 | assert(fake.component, 'missing component'); 282 | assert(fake.component.shadow, 'missing shadow'); 283 | vm.shadow = null; 284 | return Vue.nextTick(); 285 | }) 286 | .then(() => { 287 | assert(fake.component.shadow === null); 288 | }) 289 | .then(done, done); 290 | }); 291 | 292 | describe('visibility', function () { 293 | 294 | const waitUntilVisible = function (fake) { 295 | return new Promise(resolve => { 296 | fake.$watch('component.visible', () => { 297 | if (fake.component.visible) { 298 | resolve(); 299 | } 300 | }, {immediate: true}); 301 | }); 302 | }; 303 | 304 | const waitUntilInvisible = function (fake) { 305 | return new Promise(resolve => { 306 | fake.$watch('component.visible', () => { 307 | if (!fake.component.visible) { 308 | resolve(); 309 | } 310 | }, {immediate: true}); 311 | }); 312 | }; 313 | 314 | it('should be visible', function (done) { 315 | const {fake, vm, easel} = buildVm(); 316 | vm.visible = true; 317 | Vue.nextTick() 318 | .then(() => waitUntilVisible(fake)) 319 | .then(done, done); 320 | }); 321 | 322 | it('should be invisible explicitly', function (done) { 323 | const {fake, vm, easel} = buildVm(); 324 | vm.visible = false; 325 | Vue.nextTick() 326 | .then(() => waitUntilInvisible(fake)) 327 | .then(done, done); 328 | }); 329 | 330 | it('should be invisible due to visibility blocker promise', function (done) { 331 | const {fake, vm, easel} = buildVm(); 332 | vm.visible = true; 333 | fake.remainInvisibleUntil(new Promise(() => void(0))); 334 | Vue.nextTick() 335 | .then(() => waitUntilInvisible(fake)) 336 | .then(done, done); 337 | }); 338 | 339 | it('should be visible when blocker promise is already resolved', function (done) { 340 | const {fake, vm, easel} = buildVm(); 341 | vm.visible = true; 342 | fake.remainInvisibleUntil(Promise.resolve()); 343 | Vue.nextTick() 344 | .then(() => waitUntilVisible(fake)) 345 | .then(done, done); 346 | }); 347 | 348 | it('should be visible when blocker promise is already rejected', function (done) { 349 | const {fake, vm, easel} = buildVm(); 350 | vm.visible = true; 351 | fake.remainInvisibleUntil(Promise.reject()); 352 | Vue.nextTick() 353 | .then(() => waitUntilVisible(fake)) 354 | .then(done, done); 355 | }); 356 | 357 | it('should become visible when blocker promise resolves', function (done) { 358 | const {fake, vm, easel} = buildVm(); 359 | vm.visible = false; 360 | let resolve; 361 | fake.remainInvisibleUntil(new Promise(r => resolve = r)); 362 | Vue.nextTick() 363 | .then(() => waitUntilInvisible(fake)) 364 | .then(() => resolve()) 365 | .then(() => vm.visible = true) 366 | .then(() => waitUntilVisible(fake)) 367 | .then(done, done); 368 | }); 369 | 370 | it('should become visible when blocker promise rejects', function (done) { 371 | const {fake, vm, easel} = buildVm(); 372 | vm.visible = false; 373 | let reject; 374 | fake.remainInvisibleUntil(new Promise((z, r) => reject = r)); 375 | Vue.nextTick() 376 | .then(() => waitUntilInvisible(fake)) 377 | .then(() => reject()) 378 | .then(() => vm.visible = true) 379 | .then(() => waitUntilVisible(fake)) 380 | .then(done, done); 381 | }); 382 | }); 383 | 384 | // These fields are simply copied through to the component, so we test 385 | // that that copying works as intended. 386 | 387 | const passthrough = { 388 | cursor: 'pointer', 389 | rotation: 15, 390 | name: 'Charles Wallace', 391 | }; 392 | 393 | Object.keys(passthrough).forEach(function (field) { 394 | const value = passthrough[field]; 395 | 396 | it(`should have ${field} = null`, function (done) { 397 | const {fake, vm, easel} = buildVm(); 398 | Vue.nextTick() 399 | .then(() => { 400 | assert(fake.component[field] === null, `Wrong ${field}: ${fake.component[field]}`); 401 | }) 402 | .then(done, done); 403 | }); 404 | 405 | it(`should have ${field} = ${value}`, function (done) { 406 | const {fake, vm, easel} = buildVm(); 407 | vm[field] = value; 408 | Vue.nextTick() 409 | .then(() => { 410 | assert(fake.component, 'missing component'); 411 | assert(fake.component[field] === value, `Wrong ${field}: ${fake.component[field]}`); 412 | }) 413 | .then(done, done); 414 | }); 415 | 416 | it(`should have ${field} = ${value}, then null again`, function (done) { 417 | const {fake, vm, easel} = buildVm(); 418 | vm[field] = value; 419 | Vue.nextTick() 420 | .then(() => { 421 | assert(fake.component, 'missing component'); 422 | assert(fake.component[field] === value, `Wrong ${field}: ${fake.component[field]}`); 423 | vm[field] = null; 424 | return Vue.nextTick(); 425 | }) 426 | .then(() => { 427 | assert(fake.component[field] === null); 428 | }) 429 | .then(done, done); 430 | }); 431 | }); 432 | 433 | it('should default to x=0,y=0', function (done) { 434 | const {fake, vm, easel} = buildVm(); 435 | vm.x = undefined; 436 | vm.y = undefined; 437 | Vue.nextTick() 438 | .then(() => { 439 | assert(fake.component.x === 0, 'Wrong default x in: ' + fake.component.x); 440 | assert(fake.component.y === 0, 'Wrong default y in: ' + fake.component.y); 441 | }) 442 | .then(done, done); 443 | }); 444 | }; 445 | }; 446 | -------------------------------------------------------------------------------- /test/includes/is-alignable.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Vue from 'vue'; 3 | 4 | const wait = function (component) { 5 | return Promise.all([ 6 | component.getAlignDimensions(), 7 | Vue.nextTick(), 8 | ]); 9 | }; 10 | 11 | export default function isAlignable(implementor, {width, height}, extra_attributes = '', provide = {}) { 12 | 13 | return function () { 14 | 15 | const buildVm = function () { 16 | /** 17 | * A fake easel object. It allows adding and removing a child and has extra 18 | * methods to tell whether the object was added and removed. 19 | */ 20 | const easel = { 21 | addChild(vueChild) { 22 | }, 23 | removeChild(vueChild) { 24 | }, 25 | }; 26 | 27 | const vm = new Vue({ 28 | template: ` 29 | 30 | 42 | 43 | 44 | `, 45 | provide() { 46 | provide.easelParent = easel; 47 | provide.easelCanvas = easel; 48 | return provide; 49 | }, 50 | data() { 51 | return { 52 | x: 1, 53 | y: 2, 54 | eventLog: [], 55 | showFake: true, 56 | flip: '', 57 | rotation: null, 58 | scale: 1, 59 | alpha: null, 60 | shadow: null, 61 | align: ['left', 'top'], 62 | }; 63 | }, 64 | components: { 65 | implementor, 66 | }, 67 | methods: { 68 | logEvent(event) { 69 | this.eventLog.push(event); 70 | }, 71 | clearEventLog() { 72 | this.eventLog = []; 73 | }, 74 | }, 75 | }).$mount(); 76 | 77 | const fake = vm.$refs.fake; 78 | 79 | return {vm, fake}; 80 | }; 81 | 82 | /** 83 | * Alignment tests are only done here, because in some components, they 84 | * depend too much on having a real easel. 85 | */ 86 | 87 | it('should have the right given alignment', function () { 88 | const {vm, fake} = buildVm(); 89 | assert(fake.component.regX === 0, 'Wrong regX: ' + fake.component.regX); 90 | assert(fake.component.regY === 0, 'Wrong regY: ' + fake.component.regY); 91 | }); 92 | 93 | it('should be able to change the alignment', function (done) { 94 | const {vm, fake} = buildVm(); 95 | wait(fake) 96 | .then(() => { 97 | vm.align = ['right', 'bottom']; 98 | return wait(fake); 99 | }) 100 | .then(() => { 101 | assert(fake.component.regX === width, 'Wrong regX in: ' + fake.component.regX); 102 | assert(fake.component.regY === height, 'Wrong regY in: ' + fake.component.regY); 103 | }) 104 | .then(done, done); 105 | }); 106 | 107 | it('should set the right default alignment', function (done) { 108 | const {vm, fake} = buildVm(); 109 | wait(fake) 110 | .then(() => { 111 | vm.align = ['', '']; 112 | return wait(fake); 113 | }) 114 | .then(() => { 115 | assert(fake.component.regX === 0, 'Wrong regX in: ' + fake.component.regX); 116 | assert(fake.component.regY === 0, 'Wrong regY in: ' + fake.component.regY); 117 | }) 118 | .then(done, done); 119 | }); 120 | 121 | it('should normalize reversed array align prop', function (done) { 122 | const {vm, fake} = buildVm(); 123 | wait(fake) 124 | .then(() => { 125 | vm.align = ['bottom', 'right']; 126 | return wait(fake); 127 | }) 128 | .then(() => { 129 | assert(fake.component.regX === width, 'Wrong regX in: ' + fake.component.regX); 130 | assert(fake.component.regY === height, 'Wrong regY in: ' + fake.component.regY); 131 | }) 132 | .then(done, done); 133 | }); 134 | 135 | it('should normalize reversed string align prop', function (done) { 136 | const {vm, fake} = buildVm(); 137 | wait(fake) 138 | .then(() => { 139 | vm.align = 'bottom-left'; 140 | return wait(fake); 141 | }) 142 | .then(() => { 143 | assert(fake.component.regX === 0, 'Wrong regX in: ' + fake.component.regX); 144 | assert(fake.component.regY === height, 'Wrong regY in: ' + fake.component.regY); 145 | }) 146 | .then(done, done); 147 | }); 148 | 149 | it('should default undefined align prop', function (done) { 150 | const {vm, fake} = buildVm(); 151 | wait(fake) 152 | .then(() => { 153 | vm.align = ['bottom', 'right']; 154 | return wait(fake); 155 | }) 156 | .then(() => { 157 | vm.align = undefined; 158 | return wait(fake); 159 | }) 160 | .then(() => { 161 | assert(fake.component.regX === 0, 'Wrong regX in: ' + fake.component.regX); 162 | assert(fake.component.regY === 0, 'Wrong regY in: ' + fake.component.regY); 163 | }) 164 | .then(done, done); 165 | }); 166 | }; 167 | }; 168 | -------------------------------------------------------------------------------- /test/includes/is-an-easel-parent.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import EaselFake from '../fixtures/EaselFake.js'; 3 | import Vue from 'vue'; 4 | 5 | /** 6 | * Returns a function to be used with `describe` for any component using 7 | * EaselParent. 8 | * 9 | * Like this: 10 | * `describe('is a parent', isAnEaselParent(MyComponentThatIsAParent)` 11 | * @param VueComponent implementor 12 | * @return function 13 | */ 14 | export default function isAnEaselParent(implementor) { 15 | 16 | return function () { 17 | 18 | const buildVm = function () { 19 | const vm = new Vue({ 20 | template: ` 21 | 22 | 29 | 30 | 37 | 38 | 45 | 46 | 47 | `, 48 | components: { 49 | 'implementor': implementor, 50 | 'easel-fake': EaselFake, 51 | }, 52 | provide() { 53 | return { 54 | easelParent: { 55 | addChild() { 56 | }, 57 | removeChild() { 58 | }, 59 | }, 60 | easelCanvas: {}, 61 | }; 62 | }, 63 | data() { 64 | return { 65 | showOne: false, 66 | showTwo: false, 67 | list: [], 68 | }; 69 | }, 70 | }).$mount(); 71 | 72 | const parent = vm.$refs.parent; 73 | 74 | return {vm, parent}; 75 | }; 76 | 77 | it('should have 0 children', function (done) { 78 | const {vm, parent} = buildVm(); 79 | Vue.nextTick() 80 | .then(() => { 81 | assert(parent.children, 'parent has no children field'); 82 | assert(parent.children.length === 0, 'parent has too many children: ' + parent.children.length); 83 | }) 84 | .then(done, done); 85 | }); 86 | 87 | it('should get a child', function (done) { 88 | const {vm, parent} = buildVm(); 89 | vm.showOne = true; 90 | Vue.nextTick() 91 | .then(() => { 92 | assert(parent.children.length === 1, 'parent has wrong number of children: ' + parent.children.length); 93 | assert(vm.$refs.one === parent.children[0], 'parent has wrong child'); 94 | }) 95 | .then(done, done); 96 | }); 97 | 98 | it('should lose a child', function (done) { 99 | const {vm, parent} = buildVm(); 100 | vm.showOne = false; 101 | Vue.nextTick() 102 | .then(() => { 103 | assert(parent.children.length === 0, 'parent still has children: ' + parent.children.length); 104 | assert(!vm.$refs.one, 'child `one` still exists'); 105 | }) 106 | .then(done, done); 107 | }); 108 | 109 | it('should get two children', function (done) { 110 | const {vm, parent} = buildVm(); 111 | vm.showOne = true; 112 | vm.showTwo = true; 113 | Vue.nextTick() 114 | .then(() => { 115 | assert(parent.children.length === 2, 'parent does not have right children' + parent.children.length); 116 | assert(vm.$refs.one === parent.children[0], 'child `one` is not in the right place'); 117 | assert(vm.$refs.two === parent.children[1], 'child `two` is not in the right place'); 118 | }) 119 | .then(done, done); 120 | }); 121 | 122 | it('should lose two children', function (done) { 123 | const {vm, parent} = buildVm(); 124 | vm.showOne = false; 125 | vm.showTwo = false; 126 | Vue.nextTick() 127 | .then(() => { 128 | assert(parent.children.length === 0, 'parent still has children: ' + parent.children.length); 129 | assert(!vm.$refs.one, 'child `one` still exists'); 130 | assert(!vm.$refs.two, 'child `two` still exists'); 131 | }) 132 | .then(done, done); 133 | }); 134 | 135 | it('should get two children, one by one', function (done) { 136 | const {vm, parent} = buildVm(); 137 | vm.showOne = true; 138 | let one, two; 139 | Vue.nextTick() 140 | .then(() => { 141 | assert(parent.children.length === 1, 'parent does not have right children' + parent.children.length); 142 | one = vm.$refs.one; 143 | vm.showTwo = true; 144 | return Vue.nextTick(); 145 | }) 146 | .then(() => { 147 | two = vm.$refs.two 148 | assert(parent.children.length === 2, 'parent does not have right children' + parent.children.length); 149 | assert(vm.$refs.one === parent.children[0], 'child `one` is not in the right place'); 150 | assert(vm.$refs.two === parent.children[1], 'child `two` is not in the right place'); 151 | assert(one === vm.$refs.one, 'one changed to a new object'); 152 | }) 153 | .then(done, done); 154 | }); 155 | 156 | it('should get two children, one by one, in reverse', function (done) { 157 | const {vm, parent} = buildVm(); 158 | let two; 159 | vm.showOne = false; 160 | vm.showTwo = false; 161 | 162 | Vue.nextTick() 163 | .then(() => { 164 | vm.showTwo = true; 165 | return Vue.nextTick(); 166 | }) 167 | .then(() => { 168 | assert(parent.children.length === 1, 'parent does not have right children' + parent.children.length); 169 | assert(vm.$refs.two.component === parent.component.getChildAt(0), 'child `two` is not in the right place'); 170 | two = vm.$refs.two; 171 | vm.showOne = true; 172 | return Vue.nextTick(); 173 | }) 174 | .then(() => { 175 | assert(parent.children.length === 2, 'parent does not have right children' + parent.children.length); 176 | assert(vm.$refs.one.component === parent.component.getChildAt(0), 'child `one` is not in the right place'); 177 | assert(vm.$refs.two.component === parent.component.getChildAt(1), 'child `two` is not in the right place'); 178 | assert(two === vm.$refs.two, 'two changed to a new object'); 179 | }) 180 | .then(done, done); 181 | }); 182 | 183 | it('should get two children, then switch their locations', function (done) { 184 | const {vm, parent} = buildVm(); 185 | vm.showOne = false; 186 | vm.showTwo = false; 187 | 188 | Vue.nextTick() 189 | .then(() => { 190 | vm.list.push('bob'); 191 | vm.list.push('carol'); 192 | return Vue.nextTick(); 193 | }) 194 | .then(() => { 195 | assert(parent.children.length === 2, 'parent does not have right children' + parent.children.length); 196 | assert(vm.$refs.bob[0].component === parent.component.getChildAt(0), 'before switch, child `bob` is not in the right place'); 197 | assert(vm.$refs.carol[0].component === parent.component.getChildAt(1), 'before switch, child `carol` is not in the right place'); 198 | vm.list.pop(); 199 | vm.list.pop(); 200 | vm.list.push('carol'); 201 | vm.list.push('bob'); 202 | return Vue.nextTick(); 203 | }) 204 | .then(() => { 205 | assert(parent.children.length === 2, 'parent does not have right children' + parent.children.length); 206 | assert(vm.$refs.carol[0].component === parent.component.getChildAt(0), 'after switch, child `carol` is not in the right place'); 207 | assert(vm.$refs.bob[0].component === parent.component.getChildAt(1), 'after switch, child `bob` is not in the right place'); 208 | }) 209 | .then(done, done); 210 | }); 211 | 212 | it('should get the right parent on the child.component', function (done) { 213 | const {vm, parent} = buildVm(); 214 | vm.showOne = true; 215 | Vue.nextTick() 216 | .then(() => { 217 | var one = vm.$refs.one; 218 | assert(one.component.parent === parent.component); 219 | }) 220 | .then(done, done); 221 | }); 222 | }; 223 | }; 224 | -------------------------------------------------------------------------------- /test/normalize-alignment.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import normalizeAlignment, {horizontalValues, verticalValues} from '../src/libs/normalize-alignment.js'; 3 | 4 | describe('alignment-normalizer.js', function () { 5 | 6 | it('exists', function () { 7 | assert(normalizeAlignment); 8 | }); 9 | 10 | it('leaves normalized arrays alone', function () { 11 | const correct = ['left', 'top']; 12 | const normalized = normalizeAlignment(correct); 13 | assert(correct.join() === normalized.join()); 14 | }); 15 | 16 | it('normalizes backwards arrays', function () { 17 | const incorrect = ['top', 'left']; 18 | const correct = ['left', 'top']; 19 | const normalized = normalizeAlignment(incorrect); 20 | assert(correct.join() === normalized.join()); 21 | }); 22 | 23 | it('should not choke on center,center', function () { 24 | assert(normalizeAlignment(['center', 'center'])); 25 | }); 26 | 27 | it('gets upset about double vertical', function () { 28 | let caught; 29 | try { 30 | normalizeAlignment(['top', 'top']); 31 | } catch (e) { 32 | caught = e; 33 | } 34 | assert(caught, 'normalizeAlignment was fine with top,top'); 35 | }); 36 | 37 | it('gets upset about double horizontal', function () { 38 | let caught; 39 | try { 40 | normalizeAlignment(['left', 'left']); 41 | } catch (e) { 42 | caught = e; 43 | } 44 | assert(caught, 'normalizeAlignment was fine with left,left'); 45 | }); 46 | 47 | it('gets upset about unknown values', function () { 48 | let caught; 49 | try { 50 | normalizeAlignment(['no-such-value', 'no-such-value']); 51 | } catch (e) { 52 | caught = e; 53 | } 54 | assert(caught, 'normalizeAlignment was fine with no-such-value,no-such-value'); 55 | }); 56 | 57 | describe('it can handle a string in normal order', function () { 58 | const correct = ['left', 'top']; 59 | const string = 'left-top'; 60 | const normal = normalizeAlignment(string); 61 | assert(correct.join() === normal.join()); 62 | }); 63 | 64 | describe('it can handle a string in reverse order', function () { 65 | const correct = ['left', 'top']; 66 | const string = 'top-left'; 67 | const normal = normalizeAlignment(string); 68 | assert(correct.join() === normal.join()); 69 | }); 70 | 71 | describe('it can handle extra whitespace', function () { 72 | const correct = ['left', 'top']; 73 | const string = " left-top\t"; 74 | const normal = normalizeAlignment(string); 75 | assert(correct.join() === normal.join()); 76 | }); 77 | 78 | describe('it makes no change for ,center, even though they are ambiguous', function () { 79 | const correct = ['', 'center']; 80 | const normal = normalizeAlignment(correct); 81 | assert(correct.join() === normal.join()); 82 | }); 83 | 84 | describe('it makes no change for center,, even though they are ambiguous', function () { 85 | const correct = ['center', '']; 86 | const normal = normalizeAlignment(correct); 87 | assert(correct.join() === normal.join()); 88 | }); 89 | 90 | describe('is ok with all permutations, so it', function () { 91 | horizontalValues.forEach(function (h) { 92 | verticalValues.forEach(function (v) { 93 | 94 | it(`should handle [${h},${v}]`, function () { 95 | const correct = [h, v]; 96 | const normal = normalizeAlignment(correct); 97 | assert(correct.join() === normal.join()); 98 | }); 99 | 100 | it(`should handle '${h}-${v}'`, function () { 101 | const correct = [h, v]; 102 | const string = `${h}-${v}`; 103 | const normal = normalizeAlignment(string); 104 | assert(correct.join() === normal.join(), correct.join() + ' !== ' + normal.join()); 105 | }); 106 | 107 | // Only run the following tests for a pairs of values that 108 | // aren't in both sets, like '' and 'center'. 109 | // 110 | // A pair of values that ARE in both sets should remain 111 | // unchanged. 112 | const hIsAmbiguous = verticalValues.indexOf(h) > -1; 113 | const vIsAmbiguous = horizontalValues.indexOf(v) > -1; 114 | const bothAreAmbiguous = hIsAmbiguous && vIsAmbiguous; 115 | if (!bothAreAmbiguous) { 116 | it(`should handle [${v},${h}]`, function () { 117 | const correct = [h, v]; 118 | const incorrect = [v, h]; 119 | const normal = normalizeAlignment(incorrect); 120 | assert(correct.join() === normal.join()); 121 | }); 122 | 123 | it(`should handle '${v}-${h}'`, function () { 124 | const correct = [h, v]; 125 | const string = `${v}-${h}`; 126 | const normal = normalizeAlignment(string); 127 | assert(correct.join() === normal.join(), correct.join() + ' !== ' + normal.join()); 128 | }); 129 | } 130 | 131 | }); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /test/sort-by-dom.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Vue from 'vue'; 3 | import shuffle from 'lodash.shuffle'; 4 | import sortByDom, {sorter} from '../src/libs/sort-by-dom.js'; 5 | 6 | describe('sort-by-dom', function () { 7 | 8 | const nester = { 9 | props: ['name'], 10 | template: ` 11 |
12 | 13 |
14 | `, 15 | }; 16 | 17 | const vm = new Vue({ 18 | template: ` 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | `, 27 | components: { 28 | nester, 29 | }, 30 | }).$mount(); 31 | 32 | const { 33 | nester_1, 34 | nester_1_1, 35 | nester_1_2, 36 | nester_1_2_1, 37 | nester_1_3, 38 | } = vm.$refs; 39 | 40 | it('should not change the order if its already correct', function () { 41 | const correct = [ 42 | nester_1, 43 | nester_1_1, 44 | nester_1_2, 45 | nester_1_2_1, 46 | nester_1_3, 47 | ]; 48 | const sorted = sortByDom(correct); 49 | assert(correct.length === sorted.length, 'Different length correct & sorted'); 50 | sorted.forEach((element, i) => assert(element === correct[i], `error at index ${i}: wrong=${element.name}, right=${correct[i].name}`)); 51 | }); 52 | 53 | it('should put a parent before its child', function () { 54 | const correct = [ 55 | nester_1, 56 | nester_1_1, 57 | ]; 58 | const mixed = [ 59 | nester_1_1, 60 | nester_1, 61 | ]; 62 | const sorted = sortByDom(mixed); 63 | assert(correct.length === sorted.length, 'Different length correct & sorted'); 64 | sorted.forEach((element, i) => assert(element === correct[i], `error at index ${i}: wrong=${element.name}, right=${correct[i].name}`)); 65 | }); 66 | 67 | it('should put a late sibling before an early sibling', function () { 68 | const correct = [ 69 | nester_1_1, 70 | nester_1_2, 71 | ]; 72 | const mixed = [ 73 | nester_1_2, 74 | nester_1_1, 75 | ]; 76 | const sorted = sortByDom(mixed); 77 | assert(correct.length === sorted.length, 'Different length correct & sorted'); 78 | sorted.forEach((element, i) => assert(element === correct[i], `error at index ${i}: wrong=${element.name}, right=${correct[i].name}`)); 79 | }); 80 | 81 | it('should throw an exception if one is disconnected', function () { 82 | let caught; 83 | const el1 = document.createElement('img'); 84 | const array = [ 85 | {$el: el1, name: 'Disconnected image 1'}, 86 | nester_1, 87 | ]; 88 | try { 89 | sortByDom(array); 90 | } catch (e) { 91 | caught = e; 92 | } 93 | assert(caught, 'No error on disconnected'); 94 | }); 95 | 96 | it('should throw an exception if all are disconnected', function () { 97 | let caught; 98 | const el1 = document.createElement('img'); 99 | const el2 = document.createElement('img'); 100 | const array = [ 101 | {$el: el1, name: 'Disconnected image 1'}, 102 | {$el: el2, name: 'Disconnected image 2'}, 103 | ]; 104 | try { 105 | sortByDom(array); 106 | } catch (e) { 107 | caught = e; 108 | } 109 | assert(caught, 'No error on disconnected'); 110 | }); 111 | 112 | it('should not change all elements are the same element', function () { 113 | const correct = [ 114 | nester_1, 115 | nester_1, 116 | nester_1, 117 | nester_1, 118 | nester_1, 119 | ]; 120 | const sorted = sortByDom(correct); 121 | assert(correct.length === sorted.length, 'Different length correct & sorted'); 122 | sorted.forEach((element, i) => assert(element === correct[i], `error at index ${i}: wrong=${element.name}, right=${correct[i].name}`)); 123 | }); 124 | 125 | it('should sort a randomly mixed group', function () { 126 | const correct = [ 127 | nester_1, 128 | nester_1_1, 129 | nester_1_2, 130 | nester_1_2_1, 131 | nester_1_3, 132 | ]; 133 | const mixed = shuffle(correct); 134 | const sorted = sortByDom(mixed); 135 | assert(correct.length === sorted.length, 'Different length correct & sorted'); 136 | sorted.forEach((element, i) => assert(element === correct[i], `error at index ${i}: wrong=${element.name}, right=${correct[i].name}`)); 137 | }); 138 | 139 | it('should allow using the sorter in other sort functions, such as Array.sort', function () { 140 | const correct = [ 141 | nester_1, 142 | nester_1_1, 143 | nester_1_2, 144 | nester_1_2_1, 145 | nester_1_3, 146 | ]; 147 | const mixed = shuffle(correct); 148 | const sorted = mixed.sort(sorter); 149 | assert(correct.length === sorted.length, 'Different length correct & sorted'); 150 | sorted.forEach((element, i) => assert(element === correct[i], `error at index ${i}: wrong=${element.name}, right=${correct[i].name}`)); 151 | }); 152 | 153 | it('should allow using the sorter in other sort functions, such as bubble sort', function () { 154 | const correct = [ 155 | nester_1, 156 | nester_1_1, 157 | nester_1_2, 158 | nester_1_2_1, 159 | nester_1_3, 160 | ]; 161 | const mixed = shuffle(correct); 162 | const bubbleSort = function (source) { 163 | const array = [...source]; 164 | for (let i = 0; i < array.length; i++) { 165 | for (let j = i + 1; j < array.length; j++) { 166 | const compare = sorter(array[i], array[j]); 167 | if (compare <= 0) { 168 | // already correct 169 | } else { 170 | const temp = array[i]; 171 | array[i] = array[j]; 172 | array[j] = temp; 173 | } 174 | } 175 | } 176 | return array; 177 | }; 178 | const sorted = bubbleSort(mixed); 179 | assert(correct.length === sorted.length, 'Different length correct & sorted'); 180 | sorted.forEach((element, i) => assert(element === correct[i], `error at index ${i}: wrong=${element.name}, right=${correct[i].name}`)); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | // test/index.js 2 | import Vue from 'vue'; 3 | import SetShim from './fixtures/Set'; 4 | 5 | // Provide Promise since Phantom doesn't have it 6 | if (!global.Promise) { 7 | global.Promise = require('promise'); 8 | } 9 | 10 | // Provide Set since Phantom doesn't have it 11 | if (!global.Set) { 12 | global.Set = SetShim; 13 | } 14 | 15 | // Destroy Vue instances automatically: 16 | const destroyable = []; 17 | 18 | afterEach(() => { 19 | destroyable.forEach(vue => vue.$destroy()); 20 | destroyable.splice(0); 21 | }); 22 | 23 | Vue.mixin({ 24 | mounted() { 25 | destroyable.push(this); 26 | } 27 | }); 28 | 29 | // require all test files using special Webpack feature 30 | // https://webpack.github.io/docs/context.html#require-context 31 | const testsContext = require.context('.', true, /\.spec$/) 32 | testsContext.keys().forEach(testsContext) 33 | --------------------------------------------------------------------------------