diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 32e2e859..25cd5117 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -18,7 +18,7 @@ jobs: - name: 🔨 Setup Node uses: actions/setup-node@v3 with: - node-version-file: ".nvmrc" + node-version: "*" - name: 📦 Install Dependencies run: npm ci diff --git a/README.md b/README.md index cb3a3d81..003583a0 100644 --- a/README.md +++ b/README.md @@ -37,17 +37,15 @@ We use browser windows to represent screens because browsers are extremely flexi ## Installation You will need to install [node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). -Once they are installed, go to your app folder, where you want to install `wams` and run the following commands: +Then you can install this repo directly as a node module: ```bash -git clone https://github.com/hcilab/wams.git -cd wams -npm install +npm install https://github.com/hcilab/wams.git ``` ## Getting started -The easiest way to get started is to follow the [Walkthrough](#walkthrough) tutorial below. More advanced users might want to check the [code documentation](https://hcilab.github.io/wams/) and the [examples](#examples). For a taste on how WAMS works, check the [live demo section](#live-demo). +The easiest way to get started is to follow the [Walkthrough](#walkthrough) tutorial below. More advanced users might want to check the [code documentation](https://hcilab.github.io/wams/) and the [examples](#examples). ~For a taste on how WAMS works, check the [live demo section](#live-demo).~ (The live demo is currently broken). ## Examples @@ -55,14 +53,18 @@ To try out the examples, go to `examples/` and run as follows: ```bash node examples/[EXAMPLE_FILENAME] +``` ## For example: +```bash node examples/polygons.js ``` ## Live Demo +* (Currently broken) + The [live demo](https://wams-player-demo.herokuapp.com/) is an example of a video-player with a distributed user interface. First, the player controls are displayed on the screen with the video. Go to the url with a second device or browser, and as a second view is connected, the controls are automatically moved to that view. To check out the code of the live demo, see `examples/video-player.js` @@ -75,12 +77,18 @@ This walkthrough is a friendly guide on how to use most WAMS features. For more ### Set up your application -1. In the app folder, [install](#installation) WAMS, if you haven't already -2. Create an **app.js** file -3. In this file, include WAMS and initialize the application +First, let's set up a new directory for our demo application, and install WAMS into it: + +```bash +mkdir demo +cd demo +npm install https://github.com/hcilab/wams.git +``` + +Now, create an **app.js** file. In this file, include WAMS and initialize the application: ```javascript -const WAMS = require("./wams"); +const WAMS = require("wams"); const app = new WAMS.Application(); app.listen(3500); // this starts the app on port 3500, you can use any port ``` @@ -92,6 +100,7 @@ node app.js ``` And you can connect to the app using the address in the output. +- Many terminal emulators let you control+click on an address to visit it in your browser. ### Hello world @@ -105,7 +114,7 @@ const { square } = WAMS.predefined.items; app.spawn(square(200, 200, 100, "green")); ``` -This code creates a green square on the canvas with coordinates `{ x: 200, y: 200 }` and a side of `100`. +This code creates a green square on the canvas with coordinates `{ x: 200, y: 200 }` and a length of `100`. ### Hello world: Multi-Screen @@ -116,27 +125,25 @@ This example will spawn a draggable square and position connected screens in a l Put this code in your **app.js** file: ```javascript -const WAMS = require("./wams"); +const WAMS = require("wams"); const app = new WAMS.Application(); -const { items, actions } = WAMS.predefined; -const { line } = WAMS.predefined.layouts; +const { actions, items, layouts } = WAMS.predefined; function spawnSquare() { - app.spawn( - items.square(200, 200, 100, "green", { - ondrag: actions.drag, - }) - ); + const greenSquare = app.spawn(items.square(200, 200, 100, "green")); + greenSquare.on('drag', actions.drag); } -const linelayout = line(300); // 300px overlap betweens views -function handleConnect(view, device) { - view.onclick = spawnSquare; +const linelayout = layouts.line(300); // 300px overlap betweens views + +function handleConnect({ view, device }) { + view.on('click', spawnSquare); linelayout(view, device); } -app.onconnect = handleConnect; +app.on('connect', handleConnect); + app.listen(3500); ``` @@ -154,7 +161,7 @@ To test this on a single computer you could: ![Screenshot of first multiscreen app](./img/multiscreen.png) -> To try a more complex multi-screen gestures example (gestures that span multiple screens), check out `examples/shared-polygons.js` +> To try a more complex multi-screen gestures example (gestures that span multiple screens), check out `examples/shared-polygons.js`. (Currently broken). ### General Configuration of your app @@ -173,7 +180,7 @@ const app = new WAMS.Application({ staticDir: path.join(__dirname, "static"), // path to directory for static files, will be accessible at app's root status: true, // show information on current view, useful for debugging title: "Awesome App", // page title - useMultiScreenGestures: true, // enable multi-screen gestures + useMultiScreenGestures: true, // enable multi-screen gestures (currently broken) }); ``` @@ -242,6 +249,7 @@ For this example, create an `images` directory in the app folder and use it as y Put `monaLisa.jpg` from `examples/img` to the images folder. ```javascript +const path = require("node:path"); const app = WAMS.Application({ staticDir: path.join(__dirname, "./images"), }); @@ -299,71 +307,42 @@ app.spawn( > **Note** An item must have its coordinates, width and height defined to be interactive -Let's get back to our Hello world example with a green square. Just a static square is not that interesting, though. Let's make it **draggable**: +To make an item **draggable**, it's enough to attach the predefined drag action to the drag event: ```javascript -... -app.spawn(items.square(200, 200, 100, 'green', { - ondrag: actions.drag, -})); -... +const item = app.spawn(items.square(200, 200, 100, 'green')); +item.on('drag', actions.drag); ``` This looks much better. Now let's remove the square when you **click** on it. _To remove an item, use WAMS' `removeItem` method._ ```js -function handleClick(event) { - app.removeItem(event.target) -} - -app.spawn(items.square(200, 200, 100, 'green', { - ondrag: actions.drag, - onclick: handleClick, -})); -... +item.on('click', () => app.removeItem(item)); ``` -Another cool interactive feature is **rotation**. To rotate an item, first set the `onrotate` property and then grab the item with your mouse and hold **Control** key. +Another cool interactive feature is **rotation**. To rotate an item, first add a `rotate` listener, (the predefined action will do the trick), and then grab the item with your mouse and hold **Control** key. ```js -... - ondrag: actions.drag, - onclick: handleClick, - onrotate: actions.rotate, -})); -... +item.on('rotate', actions.rotate); ``` -You can also listen to **swipe** events on items (hold the item, quickly move it and release). To do that, add the `onswipe` handler. +You can also listen to **swipe** events on items (hold the item, quickly move it and release). To do that, add a `swipe` handler. ```js -... - onswipe: handleSwipe, -})); - function handleSwipe(event) { console.log(`Swipe registered!`); console.log(`Velocity: ${event.velocity}`); console.log(`Direction: ${event.direction}`); console.log(`X, Y: ${event.x}, ${event.y}`); } -... +item.on('swipe', handleSwipe); ``` To move an item, you can use `moveBy` and `moveTo` item methods: ```js -app.spawn( - image("images/monaLisa.jpg", { - width: 200, - height: 300, - onclick: handleClick, - }) -); - -function handleClick(event) { - event.target.moveBy(100, -50); -} +// do this on a different item than the one that uses removeItem +item.on('click', () => item.moveBy(100, -50)); ``` Both methods accept `x` and `y` numbers that represent a vector (for `moveBy`) or the final position (for `moveTo`). @@ -377,10 +356,10 @@ Often times, you want to use images, run custom code in the browser, or add CSS To do that, first **set up a path to the static directory:** ```javascript -const path = require("path"); +const path = require("node:path"); const app = new WAMS.Application({ - staticDir: path.join(__dirname, "./assets"), + staticDir: path.join(__dirname, "assets"), }); ``` @@ -412,10 +391,10 @@ The stylesheets will be automatically loaded by the browsers. WAMS manages all connections under the hood, and provides helpful ways to react on **connection-related events**: -- `onconnect` – called each time a screen connects to a WAMS application -- `ondisconnect` – called when a screen disconnects +- `connect` – emitted each time a screen connects to a WAMS application +- `disconnect` – emitted when a screen disconnects -Both properties can be assigned a callback function, where you can act on the event. The callback function gets an event object with these properties: +You can listen for both events on the Application instance. The handler function gets an event object with these properties: 1. `view` 2. `device` @@ -437,16 +416,16 @@ It also provides **methods** to transform the current screen's view: - `rotateBy` - `scaleBy` -And you can set up **interactions and event listeners** for the view itself: +And you can set up **event listeners** for the view itself, such as: -- `ondrag` -- `onrotate` -- `onpinch` -- `onclick` +- `drag` +- `rotate` +- `pinch` +- `click` **`Device`** stores dimensions of the screen and its original position when connected. -**`Group`** is a group of views and should be used instead of **View** _when multi-screen gestures are enabled_. +**`Group`** is a group of views and should be used instead of **View** _when multi-screen gestures are enabled_. (Multi-screen gestures are currently broken). ### Multi-Screen Layouts @@ -464,11 +443,11 @@ const { table } = WAMS.predefined.layouts; const overlap = 200; // 200px overlap between screens const setTableLayout = table(overlap); -function handleLayout(view) { - setTableLayout(view); +function handleConnect({ view, device }) { // note the {} brackets to destructure the event object + setTableLayout(view, device); } -app.onconnect = handleLayout; +app.on('connect', handleConnect); ``` To see this layout in action, check out the `card-table.js` example. @@ -486,11 +465,11 @@ const { line } = WAMS.predefined.layouts; const overlap = 200; // 200px overlap between screens const setLineLayout = line(overlap); -function handleLayout(view, device) { +function handleConnect(view, device) { setLineLayout(view, device); } -app.onconnect = handleLayout; +app.onconnect = handleConnect; ``` To see this layout in action with multi-screen gestures, check out the `shared-polygons.js` example. @@ -508,7 +487,7 @@ To spawn a custom item, use `CanvasSequence`. It allows to create a custom seque The following sequence draws a smiling face item: ```js -function smileFace(x, y) { +function smileFace(args) { const sequence = new WAMS.CanvasSequence(); sequence.beginPath(); @@ -521,25 +500,27 @@ function smileFace(x, y) { sequence.arc(90, 65, 5, 0, Math.PI * 2, true); // Right eye sequence.stroke(); - return { sequence }; + return { ...args, sequence }; } -app.spawn(smileFace(900, 300)); +app.spawn(smileFace({ x: 400, y: 300 })); ``` -You can add interactivity to a custom item the same way as with predefined items. However, you first need to add a _hitbox_ to the item: +You can add interactivity to a custom item the same way as with predefined items. However, you first need to add a _hitbox_ to the item. This can be a bit confusing, since the hitbox will always be given (x, y) values as if its item is located a (0, 0). Put another way, the hitbox doesn't need to know anything about how the item is positioned or oriented in the WAMS workspace: ```javascript -function customItem(x, y, width, height) { - const hitbox = new WAMS.Rectangle(width, height, x, y); - const ondrag = actions.drag; - - const sequence = new WAMS.CanvasSequence(); - sequence.fillStyle = "green"; - sequence.fillRect(x, y, width, height); - - return { hitbox, sequence, ondrag }; +function interactableSmileFace(args) { + const hitbox = new WAMS.Circle( + 50, // 50 is the radius of the outer circle of the smiley + 75, // the smiley is centered at (75, 75) + 75, + ); + return smileFace({ ...args, hitbox }); } + +// The Circle doesn't need to know that we're creating the smiley at (900, 300) in the workspace +const item = app.spawn(interactableSmileFace({ x: 400, y: 200 })); +item.on('drag', actions.drag); ``` A hitbox can be made from `WAMS.Rectangle` or `WAMS.Polygon2D` or `WAMS.Circle` @@ -612,7 +593,7 @@ To do that, first we'll add an index to the card item to show who its owner is. ```javascript // during creation -let card = app.spawn( +const card = app.spawn( image(url, { /* ... */ owner: 1, @@ -632,7 +613,8 @@ function flipCard(event) { if (event.view.index !== event.target.owner) return; const card = event.target; - const imgsrc = card.isFaceUp ? card_back_path : card.face; + // assume we've attach 'back' and 'face' properties to the card with paths to images + const imgsrc = card.isFaceUp ? card.back : card.face; card.setImage(imgsrc); card.isFaceUp = !card.isFaceUp; } @@ -658,14 +640,14 @@ items.push(app.spawn(square(100, 100, 200, "yellow"))); items.push(app.spawn(square(150, 150, 200, "blue"))); -const group = app.createGroup({ - items, - ondrag: actions.drag, -}); +const group = app.createGroup({ items }); +group.on('drag', actions.drag); group.moveTo(500, 300); ``` +Groups can only be moved together- rotation and scaling are not supported. + --- # Contribution diff --git a/examples/card-table.js b/examples/card-table.js index 529a82f9..b1c84553 100644 --- a/examples/card-table.js +++ b/examples/card-table.js @@ -47,7 +47,7 @@ const STYLES = { }, }; -function shuffleButton(x, y) { +function defineShuffleButton(x, y) { const width = STYLES.button.width; const height = STYLES.button.height; const button = new WAMS.CanvasSequence(); @@ -73,11 +73,10 @@ function shuffleButton(x, y) { hitbox, type: 'item', sequence: button, - onclick: dealCards, }; } -function chip(chipName, x, y) { +function defineChip(chipName, x, y) { const radius = 40; return { x, @@ -87,7 +86,6 @@ function chip(chipName, x, y) { // Yes: need to offset hitbox (x, y) to center of circle hitbox: new Circle(radius, radius, radius), - ondrag: WAMS.predefined.actions.drag, type: 'item/image', src: `Chips/${chipName}.png`, @@ -97,11 +95,13 @@ function chip(chipName, x, y) { }; } -function spawnChip(chipName, x, y) { - app.spawn(chip(chipName, x, y)); +function spawnChip(button) { + const { chipName, x, y, height } = button; + const chip = app.spawn(defineChip(chipName, x + 250, y - height / 4)); + chip.on('drag', WAMS.predefined.actions.drag); } -function chipButton(chipLabel, chipName, x, y) { +function defineChipButton(chipLabel, chipName, x, y) { const width = STYLES.button.width; const height = STYLES.button.height; const button = new WAMS.CanvasSequence(); @@ -126,14 +126,20 @@ function chipButton(chipLabel, chipName, x, y) { hitbox, type: 'item', sequence: button, - onclick: () => spawnChip(chipName, x + 250, y - height / 4), + chipName, }; } -app.spawn(shuffleButton(525, 260)); -app.spawn(chipButton('Green', 'GreenWhite_border', 525, 360)); -app.spawn(chipButton('Blue', 'BlueWhite_border', 525, 440)); -app.spawn(chipButton('Red', 'RedWhite_border', 525, 520)); +const shuffleButton = app.spawn(defineShuffleButton(525, 260)); +shuffleButton.on('click', dealCards); + +const greenButton = app.spawn(defineChipButton('Green', 'GreenWhite_border', 525, 360)); +const blueButton = app.spawn(defineChipButton('Blue', 'BlueWhite_border', 525, 440)); +const redButton = app.spawn(defineChipButton('Red', 'RedWhite_border', 525, 520)); + +greenButton.on('click', () => spawnChip(greenButton)); +blueButton.on('click', () => spawnChip(blueButton)); +redButton.on('click', () => spawnChip(redButton)); // Generate a deck of cards, consisting solely of image source paths. const values = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'A', 'J', 'Q', 'K']; @@ -158,23 +164,22 @@ function dealCards() { // Generate the cards in a random order. let offs = 0.0; shuffle(cardDescriptors).forEach((card) => { - cards.push( - app.spawn( - WAMS.predefined.items.image(cardBackImagePath, { - x: 345 - offs, - y: 300 - offs, - width: 140, - height: 190, - type: 'card', - scale: 1, - face: card, - isFaceUp: false, - onclick: flipCard, - ondrag: WAMS.predefined.actions.drag, - onrotate: WAMS.predefined.actions.rotate, - }) - ) + const cardItem = app.spawn( + WAMS.predefined.items.image(cardBackImagePath, { + x: 345 - offs, + y: 300 - offs, + width: 140, + height: 190, + type: 'card', + scale: 1, + face: card, + isFaceUp: false, + }) ); + cardItem.on('click', flipCard); + cardItem.on('drag', WAMS.predefined.actions.drag); + cardItem.on('rotate', WAMS.predefined.actions.rotate); + cards.push(cardItem); offs += 0.2; }); } @@ -197,8 +202,8 @@ function handleConnect({ view, device }) { tableLayout(view, device); } -app.onconnect = handleConnect; -app.listen(9700); +app.on('connect', handleConnect); +app.listen(9000); /** * Draws a rounded rectangle using the current state of the canvas. diff --git a/examples/checkers.js b/examples/checkers.js index 0be72587..86f4b6a3 100644 --- a/examples/checkers.js +++ b/examples/checkers.js @@ -51,7 +51,7 @@ function spawnToken(x, y, userIdx, properties = {}) { let imgUrl = userIdx === 0 ? 'Green_border.png' : 'Blue_border.png'; const radius = SQUARE_LENGTH / 2; - app.spawn({ + const token = app.spawn({ x, y, width: SQUARE_LENGTH, @@ -60,9 +60,9 @@ function spawnToken(x, y, userIdx, properties = {}) { type: 'item/image', src: imgUrl, ownerIdx: userIdx, - ondrag: (e) => handleTokenDrag(e, userIdx), ...properties, }); + token.on('drag', (e) => handleTokenDrag(e, userIdx)); } for (let i = 0; i < 10; i += 1) { @@ -95,10 +95,10 @@ function handleConnect({ view }) { centerViewNormal(view); - view.ondrag = WAMS.predefined.actions.drag; - view.onpinch = WAMS.predefined.actions.pinch; - view.onrotate = WAMS.predefined.actions.rotate; + view.on('drag', WAMS.predefined.actions.drag); + view.on('pinch', WAMS.predefined.actions.pinch); + view.on('rotate', WAMS.predefined.actions.rotate); } -app.addEventListener('connect', handleConnect); -app.listen(9012); +app.on('connect', handleConnect); +app.listen(9000); diff --git a/examples/chess.js b/examples/chess.js index 3ac2631c..c52c47ec 100644 --- a/examples/chess.js +++ b/examples/chess.js @@ -103,7 +103,7 @@ function spawnToken(x, y, userIdx, tokenIdx, properties = {}) { } } - app.spawn( + const token = app.spawn( WAMS.predefined.items.html( `
`, SQUARE_LENGTH, @@ -115,12 +115,12 @@ function spawnToken(x, y, userIdx, tokenIdx, properties = {}) { height: SQUARE_LENGTH, type, ownerIdx: userIdx, - ondrag: (e) => handleTokenDrag(e, userIdx), // rotation: event => handleRotate(event), ...properties, } ) ); + token.on('drag', (e) => handleTokenDrag(e, userIdx)); } // Spawning all pieces iteratively @@ -168,10 +168,10 @@ function handleConnect({ view }) { centerViewNormal(view); - view.ondrag = WAMS.predefined.actions.drag; - view.onpinch = WAMS.predefined.actions.pinch; - view.onrotate = WAMS.predefined.actions.rotate; + view.on('drag', WAMS.predefined.actions.drag); + view.on('pinch', WAMS.predefined.actions.pinch); + view.on('rotate', WAMS.predefined.actions.rotate); } -app.onconnect = handleConnect; -app.listen(4000); +app.on('connect', handleConnect); +app.listen(9000); diff --git a/examples/confetti-custom-event.js b/examples/confetti-custom-event.js index 8266215a..4ae988ea 100644 --- a/examples/confetti-custom-event.js +++ b/examples/confetti-custom-event.js @@ -34,20 +34,19 @@ function square(x, y, view, color) { type: 'colour', scale: 1 / view.scale, rotation: view.rotation, - ondrag: WAMS.predefined.actions.drag, }; } function spawnSquare(event, color) { - if (!color) app.spawn(square(event.x, event.y, event.view)); - else app.spawn(square(event.x, event.y, event.view, color)); + const item = app.spawn(square(event.x, event.y, event.view, color)); + item.on('drag', WAMS.predefined.actions.drag); } function handleConnect({ view }) { - view.onpinch = WAMS.predefined.actions.pinch; - view.ondrag = WAMS.predefined.actions.drag; - view.onrotate = WAMS.predefined.actions.rotate; - // view.onclick = spawnSquare; + view.on('pinch', WAMS.predefined.actions.pinch); + view.on('drag', WAMS.predefined.actions.drag); + view.on('rotate', WAMS.predefined.actions.rotate); + // view.on('click', spawnSquare); } app.on('mousedown', (event) => { @@ -69,5 +68,5 @@ app.on('mouseup', (event) => { } spawnSquare(event); }); -app.onconnect = handleConnect; -app.listen(9013); +app.on('connect', handleConnect); +app.listen(9000); diff --git a/examples/confetti.js b/examples/confetti.js index e8f966b4..1612beae 100644 --- a/examples/confetti.js +++ b/examples/confetti.js @@ -32,11 +32,11 @@ function spawnSquare(event) { } function handleConnect({ view }) { - view.onpinch = WAMS.predefined.actions.pinch; - view.ondrag = WAMS.predefined.actions.drag; - view.onrotate = WAMS.predefined.actions.rotate; - view.onclick = spawnSquare; + view.on('pinch', WAMS.predefined.actions.pinch); + view.on('drag', WAMS.predefined.actions.drag); + view.on('rotate', WAMS.predefined.actions.rotate); + view.on('click', spawnSquare); } -app.onconnect = handleConnect; -app.listen(9013); +app.on('connect', handleConnect); +app.listen(9000); diff --git a/examples/dollar_sign_and_paper.js b/examples/dollar_sign_and_paper.js index 8eabb7b1..5dc508e3 100644 --- a/examples/dollar_sign_and_paper.js +++ b/examples/dollar_sign_and_paper.js @@ -18,9 +18,9 @@ const app = new WAMS.Application({ }); function handleConnect({ view }) { - view.onpinch = WAMS.predefined.actions.pinch; - view.ondrag = WAMS.predefined.actions.drag; - view.onrotate = WAMS.predefined.actions.rotate; + view.on('pinch', WAMS.predefined.actions.pinch); + view.on('drag', WAMS.predefined.actions.drag); + view.on('rotate', WAMS.predefined.actions.rotate); } -app.onconnect = handleConnect; -app.listen(9013); +app.on('connect', handleConnect); +app.listen(9000); diff --git a/examples/drawing-app.js b/examples/drawing-app.js index b06171ca..23f1ffc2 100644 --- a/examples/drawing-app.js +++ b/examples/drawing-app.js @@ -58,8 +58,8 @@ class DrawingApp { this.app.on('set-control', this.updateControlType.bind(this)); this.app.on('set-color', this.setColor.bind(this)); this.app.on('set-width', this.setWidth.bind(this)); - this.app.onconnect = this.handleConnect.bind(this); - this.app.listen(3000); + this.app.on('connect', this.handleConnect.bind(this)); + this.app.listen(9000); } draw(event) { @@ -69,7 +69,6 @@ class DrawingApp { // const fromY = event.y - event.dy; const toX = event.x; const toY = event.y; - console.log('draw', color, width, toX, toY); const line = new CanvasSequence(); // line.beginPath() // line.moveTo(fromX, fromY); @@ -86,12 +85,13 @@ class DrawingApp { updateControlType({ type, view }) { this.controlType = type; - view.ondrag = type === 'pan' ? actions.drag : this.draw.bind(this); + view.removeAllListeners('drag'); + view.on('drag', type === 'pan' ? actions.drag : this.draw.bind(this)); } handleConnect({ view }) { - view.ondrag = WAMS.predefined.actions.drag; - view.onpinch = WAMS.predefined.actions.zoom; + view.on('drag', WAMS.predefined.actions.drag); + view.on('pinch', WAMS.predefined.actions.zoom); } } diff --git a/examples/elements.js b/examples/elements.js index 0f3d6c70..32c61230 100644 --- a/examples/elements.js +++ b/examples/elements.js @@ -17,10 +17,6 @@ function element(x, y, view) { type: 'button', scale: 1 / view.scale, rotation: view.rotation, - onpinch: WAMS.predefined.actions.pinch, - ondrag: WAMS.predefined.actions.drag, - onrotate: WAMS.predefined.actions.rotate, - onclick: removeElement, }); } @@ -29,15 +25,19 @@ function removeElement(event) { } function spawnElement(event) { - app.spawn(element(event.x, event.y, event.view)); + const item = app.spawn(element(event.x, event.y, event.view)); + item.on('pinch', WAMS.predefined.actions.pinch); + item.on('drag', WAMS.predefined.actions.drag); + item.on('rotate', WAMS.predefined.actions.rotate); + item.on('click', removeElement); } function handleConnect({ view }) { - view.onpinch = WAMS.predefined.actions.pinch; - view.ondrag = WAMS.predefined.actions.drag; - view.onrotate = WAMS.predefined.actions.rotate; - view.onclick = spawnElement; + view.on('pinch', WAMS.predefined.actions.pinch); + view.on('drag', WAMS.predefined.actions.drag); + view.on('rotate', WAMS.predefined.actions.rotate); + view.on('click', spawnElement); } -app.onconnect = handleConnect; -app.listen(9002); +app.on('connect', handleConnect); +app.listen(9000); diff --git a/examples/jumbotron.js b/examples/jumbotron.js index 8decab9d..c7f9c9a6 100644 --- a/examples/jumbotron.js +++ b/examples/jumbotron.js @@ -17,7 +17,7 @@ const app = new WAMS.Application({ const scale = 2; -app.spawn( +const lisa = app.spawn( image('monaLisa.jpg', { width: 1200, height: 1815, @@ -25,11 +25,11 @@ app.spawn( y: 0, type: 'mona', scale, - ondrag: WAMS.predefined.actions.drag, - onpinch: WAMS.predefined.actions.pinch, - onrotate: WAMS.predefined.actions.rotate, }) ); +lisa.on('drag', WAMS.predefined.actions.drag); +lisa.on('pinch', WAMS.predefined.actions.pinch); +lisa.on('rotate', WAMS.predefined.actions.rotate); const jumbotronLayout = jumbotron(1200 * scale); @@ -37,8 +37,8 @@ function handleConnect({ view }) { jumbotronLayout(view); } -app.onconnect = handleConnect; -app.listen(9010); +app.on('connect', handleConnect); +app.listen(9000); /** * Generates a handler that places devices in a jumbotron. diff --git a/examples/pairedWorkers.js b/examples/pairedWorkers.js index 71af92b5..d4f67d31 100644 --- a/examples/pairedWorkers.js +++ b/examples/pairedWorkers.js @@ -14,30 +14,30 @@ const app = new WAMS.Application({ staticDir: path.join(__dirname, './img'), }); -app.spawn( +const scream = app.spawn( image('scream.png', { x: 400, y: 400, width: 800, height: 1013, scale: 0.25, - ondrag: WAMS.predefined.actions.drag, - onrotate: WAMS.predefined.actions.rotate, - onpinch: WAMS.predefined.actions.pinch, }) ); +scream.on('drag', WAMS.predefined.actions.drag); +scream.on('rotate', WAMS.predefined.actions.rotate); +scream.on('pinch', WAMS.predefined.actions.pinch); -app.spawn( +const lisa = app.spawn( image('monaLisa.jpg', { x: 200, y: 200, width: 1200, height: 1815, scale: 0.2, - ondrag: WAMS.predefined.actions.drag, - onrotate: WAMS.predefined.actions.rotate, - onpinch: WAMS.predefined.actions.pinch, }) ); +lisa.on('drag', WAMS.predefined.actions.drag); +lisa.on('rotate', WAMS.predefined.actions.rotate); +lisa.on('pinch', WAMS.predefined.actions.pinch); -app.listen(9003); +app.listen(9000); diff --git a/examples/photo-lens.js b/examples/photo-lens.js index b6449d41..3a20fd50 100644 --- a/examples/photo-lens.js +++ b/examples/photo-lens.js @@ -25,13 +25,13 @@ app.spawn( ); function handleConnect({ view }) { - view.onrotate = WAMS.predefined.actions.rotate; + view.on('rotate', WAMS.predefined.actions.rotate); if (view.index > 0) { view.scale = 2.5; - view.ondrag = WAMS.predefined.actions.drag; - view.onpinch = WAMS.predefined.actions.pinch; + view.on('drag', WAMS.predefined.actions.drag); + view.on('pinch', WAMS.predefined.actions.pinch); } } -app.onconnect = handleConnect; -app.listen(9011); +app.on('connect', handleConnect); +app.listen(9000); diff --git a/examples/pick-n-drop.js b/examples/pick-n-drop.js index c212432a..b998d760 100644 --- a/examples/pick-n-drop.js +++ b/examples/pick-n-drop.js @@ -40,8 +40,8 @@ function handleConnect({ view, device, group }) { dimensions[view.index] = { x: device.x, y: device.y, width: device.width, height: device.height }; } -app.onconnect = handleConnect; -app.listen(9700); +app.on('connect', handleConnect); +app.listen(9000); function moveScreenToScreen(currentView, targetView) { const centeredBelowPosX = targetView.x + targetView.width / 2 - currentView.width / 2; @@ -69,10 +69,8 @@ function viewContainsItem(view, item) { } function spawnImage(x, y) { - return app.spawn( + const item = app.spawn( image('dribble.png', { - ondrag: WAMS.predefined.actions.drag, - onrotate: WAMS.predefined.actions.rotate, width: 1600, height: 1200, scale: 1 / 4, @@ -80,4 +78,7 @@ function spawnImage(x, y) { y, }) ); + item.on('drag', WAMS.predefined.actions.drag); + item.on('rotate', WAMS.predefined.actions.rotate); + return item; } diff --git a/examples/polygons.js b/examples/polygons.js index dfed0638..062b3dc7 100644 --- a/examples/polygons.js +++ b/examples/polygons.js @@ -17,10 +17,6 @@ function polygon(x, y, view) { y, type: 'colour', scale: 1 / view.scale, - onclick: removeItem, - onpinch: WAMS.predefined.actions.pinch, - onrotate: WAMS.predefined.actions.rotate, - ondrag: WAMS.predefined.actions.drag, } ); } @@ -30,15 +26,19 @@ function removeItem(event) { } function spawnItem(event) { - app.spawn(polygon(event.x, event.y, event.view)); + const item = app.spawn(polygon(event.x, event.y, event.view)); + item.on('click', removeItem); + item.on('pinch', WAMS.predefined.actions.pinch); + item.on('rotate', WAMS.predefined.actions.rotate); + item.on('drag', WAMS.predefined.actions.drag); } function handleConnect({ view }) { - view.onclick = spawnItem; - view.onpinch = WAMS.predefined.actions.pinch; - view.onrotate = WAMS.predefined.actions.rotate; - view.ondrag = WAMS.predefined.actions.drag; + view.on('click', spawnItem); + view.on('pinch', WAMS.predefined.actions.pinch); + view.on('rotate', WAMS.predefined.actions.rotate); + view.on('drag', WAMS.predefined.actions.drag); } -app.onconnect = handleConnect; -app.listen(9014); +app.on('connect', handleConnect); +app.listen(9000); diff --git a/examples/projected.js b/examples/projected.js index 1792e8e0..3e0dfcc0 100755 --- a/examples/projected.js +++ b/examples/projected.js @@ -36,14 +36,14 @@ function viewSetup({ view, device, group }) { if (view.index === 0) { view.scaleBy(0.6); } else if (view.index === 1) { - group.ondrag = WAMS.predefined.actions.drag; + group.on('drag', WAMS.predefined.actions.drag); view.scaleBy(3.4); view.moveTo(1615, 2800); } else { view.scaleBy(1.7); - view.ondrag = WAMS.predefined.actions.drag; + view.on('drag', WAMS.predefined.actions.drag); } } -app.onconnect = viewSetup; -app.listen(3500); +app.on('connect', viewSetup); +app.listen(9000); diff --git a/examples/scaffold.js b/examples/scaffold.js index 6aa3a832..1aa181cd 100644 --- a/examples/scaffold.js +++ b/examples/scaffold.js @@ -3,6 +3,6 @@ const WAMS = require('..'); const app = new WAMS.Application(); -app.onconnect = ({ view }) => {}; +app.on('connect', ({ view }) => {}); -app.listen(8080); +app.listen(9000); diff --git a/examples/shared-polygons.js b/examples/shared-polygons.js index f2a70966..0957c0f1 100644 --- a/examples/shared-polygons.js +++ b/examples/shared-polygons.js @@ -19,10 +19,6 @@ function polygon(x, y, view) { y, type: 'colour', scale: 1 / view.scale, - onclick: removeItem, - onpinch: WAMS.predefined.actions.pinch, - onrotate: WAMS.predefined.actions.rotate, - ondrag: WAMS.predefined.actions.drag, } ); } @@ -32,18 +28,22 @@ function removeItem(event) { } function spawnItem(event) { - app.spawn(polygon(event.x, event.y, event.view)); + const item = app.spawn(polygon(event.x, event.y, event.view)); + item.on('click', removeItem); + item.on('pinch', WAMS.predefined.actions.pinch); + item.on('rotate', WAMS.predefined.actions.rotate); + item.on('drag', WAMS.predefined.actions.drag); } // use predefined "line layout" const linelayout = WAMS.predefined.layouts.line(200); function handleConnect({ view, device, group }) { - group.onclick = spawnItem; - group.onpinch = WAMS.predefined.actions.pinch; - group.onrotate = WAMS.predefined.actions.rotate; - group.ondrag = WAMS.predefined.actions.drag; + group.on('click', spawnItem); + group.on('pinch', WAMS.predefined.actions.pinch); + group.on('rotate', WAMS.predefined.actions.rotate); + group.on('drag', WAMS.predefined.actions.drag); linelayout(view, device); } -app.onconnect = handleConnect; -app.listen(9500); +app.on('connect', handleConnect); +app.listen(9000); diff --git a/examples/swipe-drawing.js b/examples/swipe-drawing.js index 6ad293eb..cad3b8ea 100644 --- a/examples/swipe-drawing.js +++ b/examples/swipe-drawing.js @@ -14,8 +14,8 @@ const app = new WAMS.Application(); */ function handleSwipe({ x, y, velocity, direction }) { const cidx = Math.ceil(velocity * 10) % WAMS.colours.length; - console.count('swipes'); - console.dir({ msg: 'Swipe registered', x, y, velocity, direction }); + // console.count('swipes'); + // console.dir({ msg: 'Swipe registered', x, y, velocity, direction }); app.spawn( WAMS.predefined.items.rectangle(x, y, velocity * 25, 32, WAMS.colours[cidx], { rotation: -direction, @@ -33,14 +33,14 @@ function handleDrag({ x, y, dx, dy }) { rotation: Math.atan2(dx, dy), }) ); - console.log('Line: { x: %s, y: %s, length: %s, rotation: %s }', line.x, line.y, length, line.rotation); + // console.log('Line: { x: %s, y: %s, length: %s, rotation: %s }', line.x, line.y, length, line.rotation); setTimeout(() => app.removeItem(line), 3000); } function handleConnect({ view }) { - view.onswipe = handleSwipe; - view.ondrag = handleDrag; + view.on('swipe', handleSwipe); + view.on('drag', handleDrag); } -app.onconnect = handleConnect; -app.listen(9002); +app.on('connect', handleConnect); +app.listen(9000); diff --git a/examples/video-player.js b/examples/video-player.js index 60020671..de050d4c 100644 --- a/examples/video-player.js +++ b/examples/video-player.js @@ -50,9 +50,9 @@ class VideoPlayer { this.app.on('forward', this.handleForward.bind(this)); this.app.on('replay', this.handleReplay.bind(this)); this.app.on('video-time-sync', this.handleVideoTimeSync.bind(this)); - this.app.onconnect = this.handleConnect.bind(this); - this.app.ondisconnect = this.handleDisconnect.bind(this); - this.app.listen(3000); + this.app.on('connect', this.handleConnect.bind(this)); + this.app.on('disconnect', this.handleDisconnect.bind(this)); + this.app.listen(9000); } handlePlayerStateChange({ playing }) { @@ -110,10 +110,10 @@ class VideoPlayer { height, playing, type: 'controls', - ondrag: WAMS.predefined.actions.drag, - onpinch: WAMS.predefined.actions.pinch, }) ); + this.controls.on('drag', WAMS.predefined.actions.drag); + this.controls.on('pinch', WAMS.predefined.actions.pinch); console.log(this.controls); } diff --git a/examples/video.js b/examples/video.js index e3ee4fad..1a47a660 100644 --- a/examples/video.js +++ b/examples/video.js @@ -15,7 +15,7 @@ const topbarred = (html) =>
${html} `; @@ -30,39 +30,41 @@ const iframe = (src) => allowfullscreen >`; -app.spawn( +const moonTouchdownVideo = app.spawn( WAMS.predefined.items.html(topbarred(iframe('https://www.youtube.com/embed/RONIax0_1ec')), 560, 50, { x: X, y: 50, width: WIDTH, height: HEIGHT, type: 'video', - onpinch: WAMS.predefined.actions.pinch, - onrotate: WAMS.predefined.actions.rotate, - ondrag: WAMS.predefined.actions.drag, + lockZ: true, }) ); +moonTouchdownVideo.on('pinch', WAMS.predefined.actions.pinch); +moonTouchdownVideo.on('rotate', WAMS.predefined.actions.rotate); +moonTouchdownVideo.on('drag', WAMS.predefined.actions.drag); -app.spawn( +const falconHeavyVideo = app.spawn( WAMS.predefined.items.html(topbarred(iframe('https://www.youtube.com/embed/l5I8jaMsHYk')), 560, 50, { x: X, y: 465, width: WIDTH, height: HEIGHT, type: 'video', - onpinch: WAMS.predefined.actions.pinch, - onrotate: WAMS.predefined.actions.rotate, - ondrag: WAMS.predefined.actions.drag, + lockZ: true, }) ); +falconHeavyVideo.on('pinch', WAMS.predefined.actions.pinch); +falconHeavyVideo.on('rotate', WAMS.predefined.actions.rotate); +falconHeavyVideo.on('drag', WAMS.predefined.actions.drag); function handleConnect({ view }) { // allowing the whole view to // be moved around, rotated and scaled - view.onpinch = WAMS.predefined.actions.pinch; - view.onrotate = WAMS.predefined.actions.rotate; - view.ondrag = WAMS.predefined.actions.drag; + view.on('pinch', WAMS.predefined.actions.pinch); + view.on('rotate', WAMS.predefined.actions.rotate); + view.on('drag', WAMS.predefined.actions.drag); } -app.onconnect = handleConnect; -app.listen(9020); +app.on('connect', handleConnect); +app.listen(9000); diff --git a/examples/walkthrough-paper.js b/examples/walkthrough-paper.js index 6b798ac6..e3002eb9 100644 --- a/examples/walkthrough-paper.js +++ b/examples/walkthrough-paper.js @@ -5,20 +5,18 @@ const { square } = WAMS.predefined.items; const { line } = WAMS.predefined.layouts; function spawnSquare(event) { - app.spawn( - square(event.x - 50, event.y - 50, 100, 'green', { - ondrag: WAMS.predefined.actions.drag, - onrotate: WAMS.predefined.actions.rotate, - onpinch: WAMS.predefined.actions.pinch, - }) - ); + const item = app.spawn(square(event.x - 50, event.y - 50, 100, 'green')); + item.on('drag', WAMS.predefined.actions.drag); + item.on('rotate', WAMS.predefined.actions.rotate); + item.on('pinch', WAMS.predefined.actions.pinch); + return item; } const linelayout = line(); function handleConnect({ view, device }) { - view.onclick = spawnSquare; + view.on('click', spawnSquare); linelayout(view, device); } -app.onconnect = handleConnect; -app.listen(3600); +app.on('connect', handleConnect); +app.listen(9000); diff --git a/examples/webpage-sharing.js b/examples/webpage-sharing.js index f4bb4f3d..f6d77ebb 100644 --- a/examples/webpage-sharing.js +++ b/examples/webpage-sharing.js @@ -36,16 +36,16 @@ function bottomBarred(html) { } function spawnIframe(event, url) { - app.spawn( + const iframe = app.spawn( html(bottomBarred(``), 560, 365, { x: event.x, y: event.y, width: 560, height: 365, - ondrag: handleIframeDrag, - onclick: (e) => app.removeItem(e.target), }) ); + iframe.on('drag', handleIframeDrag); + iframe.on('click', (e) => app.removeItem(e.target)); } function setLayout(view) { @@ -61,11 +61,11 @@ function handleConnect({ view }) { setLayout(view); - view.onclick = (ev) => spawnIframe(ev, 'http://www.example.com'); + view.on('click', (ev) => spawnIframe(ev, 'http://www.example.com')); } -app.onconnect = handleConnect; -app.listen(9021); +app.on('connect', handleConnect); +app.listen(9000); function handleIframeDrag(event) { if (event.view.index !== 0 && event.target.y <= 0) { diff --git a/examples/webpages.js b/examples/webpages.js index 3e1b8028..1521e84b 100644 --- a/examples/webpages.js +++ b/examples/webpages.js @@ -11,12 +11,17 @@ const { html } = WAMS.predefined.items; // function that returns input html wrapped with a top bar function topbarred(html) { - return `${ - '
' + '
' - }${html}
`; + return `
+
+ ${html} +
`; } -app.spawn( +const gamefaqs = app.spawn( html( topbarred(''), 560, @@ -27,31 +32,33 @@ app.spawn( width: 560, height: 365, type: 'video', - onpinch: WAMS.predefined.actions.pinch, - onrotate: WAMS.predefined.actions.rotate, - ondrag: WAMS.predefined.actions.drag, + lockZ: true, } ) ); +gamefaqs.on('pinch', WAMS.predefined.actions.pinch); +gamefaqs.on('rotate', WAMS.predefined.actions.rotate); +gamefaqs.on('drag', WAMS.predefined.actions.drag); -app.spawn( +const xkcd = app.spawn( html(topbarred(''), 560, 50, { x: 400, y: 465, width: 560, height: 365, type: 'video', - onpinch: WAMS.predefined.actions.pinch, - onrotate: WAMS.predefined.actions.rotate, - ondrag: WAMS.predefined.actions.drag, + lockZ: true, }) ); +xkcd.on('pinch', WAMS.predefined.actions.pinch); +xkcd.on('rotate', WAMS.predefined.actions.rotate); +xkcd.on('drag', WAMS.predefined.actions.drag); function handleConnect({ view }) { - view.onpinch = WAMS.predefined.actions.pinch; - view.onrotate = WAMS.predefined.actions.rotate; - view.ondrag = WAMS.predefined.actions.drag; + view.on('pinch', WAMS.predefined.actions.pinch); + view.on('rotate', WAMS.predefined.actions.rotate); + view.on('drag', WAMS.predefined.actions.drag); } -app.onconnect = handleConnect; -app.listen(9021); +app.on('connect', handleConnect); +app.listen(9000); diff --git a/src/mixins/EventTarget.js b/src/mixins/EventTarget.js index 5f827c13..1e2a3bf8 100644 --- a/src/mixins/EventTarget.js +++ b/src/mixins/EventTarget.js @@ -1,6 +1,6 @@ 'use strict'; -const EventTarget = (superclass) => +const EventTarget = (superclass) => { class EventTarget extends superclass { constructor(...args) { super(...args); @@ -56,6 +56,30 @@ const EventTarget = (superclass) => this.listeners[event].splice(index, 1); return true; } - }; + + /** + * Remove all listeners, or those of the specified `eventName` + * + * @param {string} eventName optional + */ + removeAllListeners(eventName) { + if (eventName === undefined) { + this.listeners = {}; + } else { + delete this.listeners[eventName]; + } + } + + /** + * @returns {string[]} an array listing the events for which the target has registered listeners. + */ + eventNames() { + return Object.keys(this.listeners); + } + } + EventTarget.prototype.on = EventTarget.prototype.addEventListener; + EventTarget.prototype.off = EventTarget.prototype.removeEventListener; + return EventTarget; +}; module.exports = EventTarget; diff --git a/src/server/Application.js b/src/server/Application.js index 59ecec7d..62678800 100644 --- a/src/server/Application.js +++ b/src/server/Application.js @@ -6,7 +6,7 @@ const os = require('os'); const IO = require('socket.io'); // Local classes, etc -const { constants } = require('../shared.js'); +const { constants, DataReporter, Message } = require('../shared.js'); const Router = require('./Router.js'); const Switchboard = require('./Switchboard.js'); const WorkSpace = require('./WorkSpace.js'); @@ -214,16 +214,6 @@ class Application extends EventTarget(Object) { }); new Message(Message.DISPATCH, dreport).emitWith(this.workspace.namespace); } - - /** - * Set up a custom Server event listener. - * - * @param {*} event name of the custom Server event. - * @param {*} handler handler of the custom event. - */ - on(event, handler) { - this.addEventListener(event, handler); - } } module.exports = Application; diff --git a/src/server/ServerGroup.js b/src/server/ServerGroup.js index 995b3c71..197fe17a 100644 --- a/src/server/ServerGroup.js +++ b/src/server/ServerGroup.js @@ -35,17 +35,15 @@ class ServerGroup extends Identifiable(Hittable(Item)) { this.setMeasures(); this.setParentForItems(); - - this.setupInteractions(); } - setupInteractions() { - if (this.ondrag) { - this.items.forEach((item) => { - // trying to drag any of the items will drag the whole group - item.ondrag = this.ondrag; + on(eventName, listener) { + this.items.forEach((item) => { + item.on(eventName, (event) => { + event.target = this; + return listener(event); }); - } + }); } /* diff --git a/src/server/ServerView.js b/src/server/ServerView.js index 0c7f6c56..024f91e6 100644 --- a/src/server/ServerView.js +++ b/src/server/ServerView.js @@ -1,7 +1,7 @@ 'use strict'; const { DataReporter, IdStamper, Message, View } = require('../shared.js'); -const { Interactable, Locker } = require('../mixins.js'); +const { Interactable, Locker, EventTarget } = require('../mixins.js'); const STAMPER = new IdStamper(); @@ -18,7 +18,7 @@ const STAMPER = new IdStamper(); * @param {Object} [ values ] - Object with user supplied values describing the * view. */ -class ServerView extends Locker(Interactable(View)) { +class ServerView extends Locker(Interactable(EventTarget(View))) { constructor(socket, values = {}) { super(values); diff --git a/src/server/WorkSpace.js b/src/server/WorkSpace.js index 5f6fa4c6..a8f2e1b3 100644 --- a/src/server/WorkSpace.js +++ b/src/server/WorkSpace.js @@ -92,7 +92,7 @@ class WorkSpace { const itemClass = item.constructor.name; if (itemClass !== 'ServerView' && itemClass !== 'ServerViewGroup') { if (!item.lockZ) this.raiseItem(item); - if (item.ondrag || item.onpinch || item.onrotate) { + if (this._canLock(item)) { view.obtainLockOnItem(item); } else { view.obtainLockOnItem(view); @@ -102,6 +102,20 @@ class WorkSpace { } } + _canLock(item) { + const eventNames = item.eventNames(); + return ( + item.ondrag || + item.onpinch || + item.onrotate || + item.onswipe || + eventNames.includes('drag') || + eventNames.includes('pinch') || + eventNames.includes('rotate') || + eventNames.includes('swipe') + ); + } + /** * Raises item above others and notifies subscribers. *