diff --git a/README.md b/README.md index 8943a92..22a3635 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,42 @@ Looks like this: ![img](http://i.imgur.com/1LCZxrg.png) + +Or in case the canvas element appears later, for example, when it's created by DOM driver: + +```jsx +import {run} from '@cycle/rxjs-run'; +import {makeDOMDriver} from '@cycle/dom' +import {makeDynamicHostCanvasDriver, rect} from 'cycle-canvas'; +import {Observable} from 'rxjs' + +function main (sources) { + const hostCanvas$ = sources.DOM.select('canvas').element() + const rootElement$ = Observable.of(rect({ + x: 10, + y: 10, + width: 160, + height: 100, + draw: [ + {fill: 'purple'} + ] + })) + return { + DOM: Observable.of(), + Canvas: Observable.combineLatest(hostCanvas$, rootElement$).map(([hostCanvas, rootElement]) => { + return {hostCanvas, rootElement} + }) + }; +} + +const drivers = { + DOM: makeDOMDriver('body'), + Canvas: makeDynamicHostCanvasDriver() +}; + +run(main, drivers); +``` + Also check out the [flappy bird example](https://cyclejs-community.github.io/cycle-canvas/). You can find the source for flappy bird [here](https://github.com/cyclejs-community/cycle-canvas/tree/master/examples/flappy-bird). @@ -73,10 +109,7 @@ You can find the source for flappy bird [here](https://github.com/cyclejs-commun #### Creating a canvas driver - [`makeCanvasDriver`](#makeCanvasDriver) - -#### Using event streams of the canvas element - -- [`sources.Canvas.events`](#events) +- [`makeDynamicHostCanvasDriver`](#makeDynamicHostCanvasDriver) #### Drawing shapes and text @@ -109,13 +142,11 @@ The input to this driver is a stream of drawing instructions and transformations - `selector: string` a css selector to use in order to find a canvas to attach the driver to. - `canvasSize: {width: integer, height: integer}` an object that denotes the size to set for the attached canvas. If null, the driver attaches to its canvas without altering its size. -## Using event streams of the canvas element - -### `sources.Canvas.events(eventName)` +#### Listening to event streams of the canvas element: `sources.Canvas.events(eventName)` -Canvas driver exposes a source object with an `events` method, which works similarly to the `events` method of the DOM driver. +Canvas driver produced by `makeCanvasDriver` function exposes a source object with an `events` method, which works similarly to the `events` method of the DOM driver. -#### Example: +##### Example: ```js import {run} from '@cycle/rxjs-run'; import {makeCanvasDriver, rect, text} from 'cycle-canvas'; @@ -150,6 +181,21 @@ const drivers = { run(main, drivers); ``` +### `makeDynamicHostCanvasDriver()` + +An alternative factory for the canvas driver function. + +Does not take any arguments, but requires events in the sink to be of this format: +```js +{ + hostCanvas + rootElement +} +``` +where `hostCanvas` is a `` DOM element and `rootElement` is the element to draw on the `hostCanvas`. + +You can find an [example](#makeDynamicHostCanvasDriverExample) at the top. + ## Drawing shapes and text ### `rect(params = {})` @@ -171,24 +217,24 @@ Draws a rectangle given an object containing drawing parameters. #### Example: ```js rect({ - x: 10, - y: 10, - width: 100, - height: 100, - draw: [ - {fill: 'purple'} - ], - children: [ - rect({ - x: 20, - y: 20, - width: 50, - height: 50, - draw: [ - {fill: 'blue'} - ] - }) - ] + x: 10, + y: 10, + width: 100, + height: 100, + draw: [ + {fill: 'purple'} + ], + children: [ + rect({ + x: 20, + y: 20, + width: 50, + height: 50, + draw: [ + {fill: 'blue'} + ] + }) + ] }) ``` @@ -212,19 +258,19 @@ Draws line(s) given an object containing drawing parameters. #### Example: ```js line({ - x: 10, - y: 10, - style: { - lineWidth: 2, - lineCap: 'square', - strokeStyle: '#CCCCCC' - }, - points: [ - {x: 10, y: 10}, - {x: 10, y: 20}, - {x: 20, y: 10}, - {x: 10, y: 10} - ] + x: 10, + y: 10, + style: { + lineWidth: 2, + lineCap: 'square', + strokeStyle: '#CCCCCC' + }, + points: [ + {x: 10, y: 10}, + {x: 10, y: 20}, + {x: 20, y: 10}, + {x: 10, y: 10} + ] }) ``` @@ -272,17 +318,17 @@ Draws line(s) given an object containing drawing parameters. #### Example: ```js polygon({ - points: [ - {x: 10, y: 0}, - {x: 0, y: 10}, - {x: 0, y: 30}, - {x: 30, y: 30}, - {x: 30, y: 10} // a house shaped polygon - ], - draw: { - stroke: '#000', - fill: '#ccc' - }, + points: [ + {x: 10, y: 0}, + {x: 0, y: 10}, + {x: 0, y: 30}, + {x: 30, y: 30}, + {x: 30, y: 10} // a house shaped polygon + ], + draw: { + stroke: '#000', + fill: '#ccc' + }, }) ``` @@ -304,13 +350,13 @@ Draws text given an object containing drawing parameters. #### Example: ```js text({ - x: 10, - y: 10, - value: 'Hello World!', - font: '18pt Arial', - draw: [ - {fill: 'white'} - ] + x: 10, + y: 10, + value: 'Hello World!', + font: '18pt Arial', + draw: [ + {fill: 'white'} + ] }) ``` @@ -333,8 +379,8 @@ Draws an image given an object containing drawing parameters. #### Example: ```js image({ - x: 10, - y: 10, + x: 10, + y: 10, src: document.querySelector('img') }) ``` @@ -348,18 +394,18 @@ Moves the canvas origin to a different point. #### Example: ```js - rect({ - transformations: [ + rect({ + transformations: [ {translate: {x: 10, y: 10}} ], - x: 100, - y: 100, - width: 150, - height: 150, - draw: [ - {fill: 'purple'} - ] - }) + x: 100, + y: 100, + width: 150, + height: 150, + draw: [ + {fill: 'purple'} + ] + }) ``` ### `rotate: number` @@ -368,18 +414,18 @@ Rotate the canvas around the current origin. #### Example: ```js - rect({ - transformations: [ - {rotate: (20*Math.PI/180)} + rect({ + transformations: [ + {rotate: (20*Math.PI/180)} ], - x: 10, - y: 10, - width: 150, - height: 150, - draw: [ - {fill: 'purple'} - ] - }) + x: 10, + y: 10, + width: 150, + height: 150, + draw: [ + {fill: 'purple'} + ] + }) ``` ### `scale: {x: number, y: number}` @@ -388,18 +434,18 @@ Scales the drawing bigger or smaller. #### Example: ```js - rect({ - transformations: [ - {scale: {x: 2, y: 2}}, + rect({ + transformations: [ + {scale: {x: 2, y: 2}}, ], - x: 10, - y: 10, - width: 150, - height: 150, - draw: [ - {fill: 'purple'} - ] - }) + x: 10, + y: 10, + width: 150, + height: 150, + draw: [ + {fill: 'purple'} + ] + }) ``` ### Combining transformations @@ -408,17 +454,17 @@ Scales the drawing bigger or smaller. Rotate around the point (100, 100) and draw a 50x50px box centered there: ```js - rect({ - transformations: [ + rect({ + transformations: [ {translate: {x: 100, y: 100}}, {rotate: (20*Math.PI/180)} ], - x: -25, // At this point, {x: 0, y: 0} is a point on position {x: 100, y: 100} of the canvas - y: -25, - width: 50, - height: 50, - draw: [ - {fill: 'purple'} - ] - }) + x: -25, // At this point, {x: 0, y: 0} is a point on position {x: 100, y: 100} of the canvas + y: -25, + width: 50, + height: 50, + draw: [ + {fill: 'purple'} + ] + }) ``` diff --git a/package-lock.json b/package-lock.json index 6cd210b..1689a44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,16 +11,16 @@ "dev": true }, "@cycle/dom": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@cycle/dom/-/dom-15.2.0.tgz", - "integrity": "sha1-lTMXD2MSkv1LhZHKaXuG9OHrZoY=", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@cycle/dom/-/dom-19.3.0.tgz", + "integrity": "sha512-kY/vngWfrIEwoXGtekxcl0EfE4sZ83zsDOEYEgIIaJbrDuKQJdU5kjJm4mCXDWVOscab8J6Rw/tVA8sdayOdHg==", "dev": true, "requires": { - "@cycle/run": "3.2.0", + "@cycle/run": "3.4.0", "es6-map": "0.1.5", - "snabbdom": "0.6.5", - "snabbdom-selector": "1.1.1", - "snabbdom-to-html": "3.0.1" + "snabbdom": "0.7.0", + "snabbdom-selector": "2.0.1", + "xstream": "10.9.0" } }, "@cycle/isolate": { @@ -30,9 +30,9 @@ "dev": true }, "@cycle/run": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@cycle/run/-/run-3.2.0.tgz", - "integrity": "sha1-+4qq6eY31iH3i3adOuGFiB7JKJc=", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@cycle/run/-/run-3.4.0.tgz", + "integrity": "sha512-YUZyPu0nC4YDC31mLH5PGxbMoPEH5dNEV+nmgt34GgGgJ0ykDd4PrY7/ph5MAEpQE6rOfov0VN44qQRs6beQow==", "dev": true, "requires": { "xstream": "10.9.0" @@ -1211,12 +1211,6 @@ } } }, - "browser-split": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/browser-split/-/browser-split-0.0.1.tgz", - "integrity": "sha1-ewl1dPjj6tYG+0Zk5krf3aKYGpM=", - "dev": true - }, "browserify": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/browserify/-/browserify-12.0.2.tgz", @@ -1913,6 +1907,15 @@ "through": "2.3.8" } }, + "cssauron2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cssauron2/-/cssauron2-2.0.1.tgz", + "integrity": "sha512-gz+Mvx1GGT9dwEdFi6/w/cAP3hUeTM0UXWpELDC/uIwmUm96HLNckVcrav6NC7BLmEI/KbXEpjfVLEkupU6azw==", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, "cssom": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", @@ -1959,7 +1962,7 @@ "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "dev": true, "requires": { - "es5-ext": "0.10.30" + "es5-ext": "0.10.35" } }, "dashdash": { @@ -2334,23 +2337,23 @@ } }, "es5-ext": { - "version": "0.10.30", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.30.tgz", - "integrity": "sha1-cUGhaDZpfbq/qq7uQUlc4p9SyTk=", + "version": "0.10.35", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.35.tgz", + "integrity": "sha1-GO6FjOajxFx9eekcFfzKnsVoSU8=", "dev": true, "requires": { - "es6-iterator": "2.0.1", + "es6-iterator": "2.0.3", "es6-symbol": "3.1.1" } }, "es6-iterator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", - "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.30", + "es5-ext": "0.10.35", "es6-symbol": "3.1.1" } }, @@ -2361,8 +2364,8 @@ "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-iterator": "2.0.1", + "es5-ext": "0.10.35", + "es6-iterator": "2.0.3", "es6-set": "0.1.5", "es6-symbol": "3.1.1", "event-emitter": "0.3.5" @@ -2375,8 +2378,8 @@ "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-iterator": "2.0.1", + "es5-ext": "0.10.35", + "es6-iterator": "2.0.3", "es6-symbol": "3.1.1", "event-emitter": "0.3.5" } @@ -2388,7 +2391,7 @@ "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.30" + "es5-ext": "0.10.35" } }, "escape-html": { @@ -2773,7 +2776,7 @@ "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.30" + "es5-ext": "0.10.35" } }, "events": { @@ -5249,48 +5252,18 @@ "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", "dev": true }, - "lodash.escape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", - "integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=", - "dev": true - }, - "lodash.forown": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.forown/-/lodash.forown-4.4.0.tgz", - "integrity": "sha1-hRFc8E9z75ZuztUlEdOJPMRmg68=", - "dev": true - }, - "lodash.kebabcase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=", - "dev": true - }, "lodash.memoize": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", "dev": true }, - "lodash.remove": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.remove/-/lodash.remove-4.7.0.tgz", - "integrity": "sha1-8x0x58OaBpDVB07A02JxYjNO5iY=", - "dev": true - }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "dev": true }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, "lodash.uniqby": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz", @@ -5878,15 +5851,6 @@ "error-ex": "1.3.1" } }, - "parse-sel": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-sel/-/parse-sel-1.0.0.tgz", - "integrity": "sha1-uTANK7lGoGwiyY4gjkeyCIaQy90=", - "dev": true, - "requires": { - "browser-split": "0.0.1" - } - }, "parse5": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.2.tgz", @@ -6800,33 +6764,18 @@ "dev": true }, "snabbdom": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-0.6.5.tgz", - "integrity": "sha1-AbDLqNYj7KGeVwh2YwwSwFeDAGY=", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-0.7.0.tgz", + "integrity": "sha512-LCg6lH9p2OD5n52SI4LlpYmDW2bscxsyN7rhnGJB/R3LQy/FdJfqNBM5aVST+zOfM4OdKFl8pxVUhjGsPtQA1w==", "dev": true }, "snabbdom-selector": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/snabbdom-selector/-/snabbdom-selector-1.1.1.tgz", - "integrity": "sha1-veZvxUs07/wvahPUp03g08x3G/0=", - "dev": true, - "requires": { - "cssauron": "1.4.0" - } - }, - "snabbdom-to-html": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snabbdom-to-html/-/snabbdom-to-html-3.0.1.tgz", - "integrity": "sha1-bsAAxKlHLwbhJxXrrB6UWxF09O0=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/snabbdom-selector/-/snabbdom-selector-2.0.1.tgz", + "integrity": "sha512-GLa3lvATTR9DcsAmH7fHoEu3SK2ureTPGo0SwaJGJ7z54rkIB9gALEuInKxVRvuanMVQLe4IXEAMkuq+/Nr/Mw==", "dev": true, "requires": { - "lodash.escape": "4.0.1", - "lodash.forown": "4.4.0", - "lodash.kebabcase": "4.1.1", - "lodash.remove": "4.7.0", - "lodash.uniq": "4.5.0", - "object-assign": "4.1.1", - "parse-sel": "1.0.0" + "cssauron2": "2.0.1" } }, "sntp": { diff --git a/package.json b/package.json index 63ea169..538d495 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ }, "homepage": "https://github.com/Widdershin/cycle-canvas", "devDependencies": { - "@cycle/dom": "^15.0.0", + "@cycle/dom": "^19.3.0", "@cycle/isolate": "^2.0.0", "@cycle/rxjs-run": "^4.1.0", "@cycle/time": "^0.7.3", diff --git a/src/canvas-driver.js b/src/canvas-driver.js index 82e7b2e..35bb44c 100644 --- a/src/canvas-driver.js +++ b/src/canvas-driver.js @@ -397,40 +397,25 @@ export function image (opts) { } export function makeCanvasDriver (selector, canvasSize = null) { - let canvas = root.document.querySelector(selector) + let hostCanvas = root.document.querySelector(selector) - if (!canvas) { - canvas = root.document.createElement('canvas') + if (!hostCanvas) { + hostCanvas = root.document.createElement('canvas') root.document.body.appendChild(canvas) } if (canvasSize) { - canvas.width = canvasSize.width - canvas.height = canvasSize.height + hostCanvas.width = canvasSize.width + hostCanvas.height = canvasSize.height } - const context = canvas.getContext('2d') + const context = hostCanvas.getContext('2d') let driver = function canvasDriver (sink$) { sink$.addListener({ next: rootElement => { - const defaults = { - kind: 'rect', - x: 0, - y: 0, - width: canvas.width, - height: canvas.height, - draw: [ - {clear: true} - ] - } - - const rootElementWithDefaults = Object.assign( - {}, - defaults, - rootElement - ) + const rootElementWithDefaults = getRootElementWithDefaults(hostCanvas, rootElement) const instructions = translateVtreeToInstructions(rootElementWithDefaults) @@ -441,9 +426,50 @@ export function makeCanvasDriver (selector, canvasSize = null) { }) return { - events: eventName => adapt(fromEvent(canvas, eventName)) + events: eventName => adapt(fromEvent(hostCanvas, eventName)) } } return driver } + +export function makeDynamicHostCanvasDriver () { + let driver = function dynamicHostCanvasDriver (sink$) { + sink$.addListener({ + next: ({hostCanvas, rootElement}) => { + const context = hostCanvas.getContext('2d') + + const rootElementWithDefaults = getRootElementWithDefaults(hostCanvas, rootElement) + + const instructions = translateVtreeToInstructions(rootElementWithDefaults) + + renderInstructionsToCanvas(instructions, context) + }, + error: e => { throw e }, + complete: () => null + }) + + return adapt(xs.empty()) + } + + return driver +} + +function getRootElementWithDefaults(hostCanvas, rootElement) { + const defaults = { + kind: 'rect', + x: 0, + y: 0, + width: hostCanvas.width, + height: hostCanvas.height, + draw: [ + {clear: true} + ] + } + + return Object.assign( + {}, + defaults, + rootElement + ) +} diff --git a/test/driver-test.js b/test/driver-test.js index 8de686c..170a80b 100644 --- a/test/driver-test.js +++ b/test/driver-test.js @@ -1,5 +1,5 @@ /* globals describe, it */ -import {translateVtreeToInstructions, renderInstructionsToCanvas, rect, line, arc, text, polygon, image, makeCanvasDriver} from '../src/canvas-driver' +import {translateVtreeToInstructions, renderInstructionsToCanvas, rect, line, arc, text, polygon, image, makeCanvasDriver, makeDynamicHostCanvasDriver} from '../src/canvas-driver' import assert from 'assert' import root from 'window-or-global' import {JSDOM} from 'jsdom' @@ -963,4 +963,59 @@ describe('canvasDriver', () => { canvasEl.dispatchEvent(event) }) }) + + describe('makeDynamicHostCanvasDriver', () => { + it('produces a canvas driver, a sink to which must emit hostCanvas DOM element along with rootElement to draw on that canvas', () => { + const jsdom = new JSDOM('') + + const actualContextOperations = [] + const context = mockContext(operation => actualContextOperations.push(operation)) + + const hostCanvas = jsdom.window.document.querySelector('canvas') + hostCanvas.getContext = which => which === '2d' && context + + const rootElement = rect({ + x: 10, + y: 20, + width: 30, + height: 40, + draw: [{fill: 'black'}] + }) + + const vcanvas$ = xs.of({ + hostCanvas, + rootElement + }) + + makeDynamicHostCanvasDriver()(vcanvas$) + + const expectedContextOperations = [ + {call: 'save', args: []}, + {set: 'lineWidth', value: 1}, + {set: 'fillStyle', value: 'black'}, + {call: 'fillRect', args: [10, 20, 30, 40]}, + {call: 'restore', args: []} + ] + + assert.deepEqual(actualContextOperations, expectedContextOperations) + }) + }) + + function mockContext(operationCallback) { + return new Proxy({}, { + get: (_, methodName) => { + return (...args) => operationCallback({ + call: methodName, + args + }) + }, + set: (_, propName, value) => { + operationCallback({ + set: propName, + value + }) + return true + } + }) + } })