├── LICENSE ├── README.md ├── data-watching ├── index.html ├── sketch.js └── vue-definitions.js ├── interactive-narrative-2 ├── index.html ├── p5-vue-component.js ├── scene-logic.js ├── sketch1.js ├── sketch2.js ├── sketch3.js └── style.css ├── interactive-narrative ├── index.html ├── p5-vue-component.js ├── scene-logic.js ├── sketch.js └── style.css ├── one-way-binding ├── index.html ├── sketch1.js ├── sketch2.js └── vue-definitions.js ├── two-way-binding ├── index.html ├── sketch1.js ├── sketch2.js └── vue-definitions.js └── whale-story ├── index.html ├── normalize.css ├── p5-vue-component.js ├── scene-logic.js ├── sketch.js ├── style.css └── whalestory.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Aatish Bhatia 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.md: -------------------------------------------------------------------------------- 1 | # p5-vue-starter 2 | 3 | This is a set of examples for how to use [Vue.js](https://vuejs.org/) and [p5.js](https://p5js.org/) together, so that you can build interactive documents that include p5.js sketches as components. 4 | 5 | ## Try it out 6 | - [One way binding](https://aatishb.com/p5-vue-starter/one-way-binding/) 7 | - [Two way binding](https://aatishb.com/p5-vue-starter/two-way-binding/) 8 | - [Data Watching](https://aatishb.com/p5-vue-starter/data-watching/) 9 | - [Interactive Narrative (Single Canvas in background)](https://aatishb.com/p5-vue-starter/interactive-narrative/) 10 | - [Interactive Narrative (Multiple Canvases)](https://aatishb.com/p5-vue-starter/interactive-narrative-2/) 11 | - [Interactive Narrative (Whale Story)](https://aatishb.com/p5-vue-starter/whale-story/) 12 | 13 | ## One Way Binding Between Parent & p5 Sketch 14 | 15 | In [this example](https://aatishb.com/p5-vue-starter/one-way-binding/), the p5 sketch reacts to data in the top layer. The p5 sketches are loaded in a [custom component](https://vuejs.org/v2/guide/components.html), created using the following command: 16 | 17 | ``` 18 | 19 | ``` 20 | 21 | where `sketch.js` points to the file containing the p5 code (written in ['instance mode'](https://github.com/processing/p5.js/wiki/Global-and-instance-mode)), and the sketch is being passed a `data` object. 22 | 23 | We can then access the data in the p5 sketch using the variable `parent.data`. Since this is an object, `parent.data.x` and `parent.data.y` will give you the individual x & y values. 24 | 25 | **Tip:** Ideally, the sketches should not modify the `data` directly, in keeping with the principle of [one-way data flow](https://vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow). Although this will technically work (i.e., if you modify the parent data in the child sketch, the other components will react accordingly), this is considered a [bad practice](https://antenna.io/blog/2018/01/state-management-in-vue-js) in Vue as it can easily lead to bugs. 26 | 27 | 28 | ## Two Way Binding Between Parent & p5 Sketch 29 | 30 | Sometimes, we might want two-way communication between the p5 sketches and the parent, where the sketches can react to the data *and* update the data as well. Instead of directly modifying the data object from the sketch, a better practice is for the sketch component to emit an update event which asks the top layer to change the data. We listen for this event in the top layer, and respond by updating the data. This sounds complicated, but Vue makes this quite easy to do. 31 | 32 | To set this up in Vue, we need to create the p5 component as follows: 33 | 34 | ``` 35 | 36 | ``` 37 | 38 | Where the [.sync](https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier) part tells the parent to listen to events from the p5 component and update the data accordingly. 39 | 40 | Now, in the p5 code, if we want to change the data, we can emit an update event like this: 41 | 42 | ``` 43 | parent.$emit('update:x', 100); 44 | ``` 45 | which updates the value of `data.x` to 100. Here's [an example](https://aatishb.com/p5-vue-starter/two-way-binding/) of two way binding in action. 46 | 47 | **Heads up:** Be careful with two way binding! It's easy to accidentally create a situation where your sketches are sending conflicting update messages. If you are using mouse/touch input, it's a good idea to check that the input is coming from within the canvas of your sketch before sending an update event, like in [this example](https://github.com/aatishb/p5-vue-starter/blob/master/two-way-binding/sketch1.js#L20-L21). 48 | 49 | ## No Binding Between Parent & p5 Sketch 50 | 51 | If you want to create multiple *independent* p5 canvases on a single page, and you don't need to share data between components, then using a framework like Vue is probably overkill. Instead, take a look at [this tutorial](http://joemckaystudio.com/multisketches/). Howevever, you could do this here like this: 52 | 53 | ``` 54 | 55 | ``` 56 | where we are loading the component but not passing it any data. 57 | 58 | 59 | ## Data Watching 60 | 61 | OK, so you've bound a p5 sketch to some data using the method above. Now, say you want your sketch to run some code whenever the data changes. Normally, you might do this by checking for a change in the data on each frame of the draw() loop. This approach can be computationally expensive, because it requires making a comparison for each frame of your draw loop (or about 60 comparisons a second). 62 | 63 | Vue provides a more efficient solution. By using a [watcher](https://vuejs.org/v2/guide/computed.html#Watchers), we can execute code *only* when the data changes. Watchers are useful when you want to perform expensive computations in response to changing data. 64 | 65 | To do this, we've added a custom p5 function called `dataChanged()`. Any code placed in this function will only be executed when the data changes. 66 | 67 | ``` 68 | p.dataChanged = function(val, oldVal) { 69 | // any code here only runs when the data changes 70 | }; 71 | 72 | ``` 73 | 74 | Here's [an example](https://aatishb.com/p5-vue-starter/data-watching/) of this in action. 75 | 76 | ## Code 77 | 78 | Code available on [Github](https://github.com/aatishb/p5-vue-starter) 79 | -------------------------------------------------------------------------------- /data-watching/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |

Data Watching Example

15 | 16 |

Slide me!

17 | 18 | 19 | 20 | 21 | 22 |

Edit me!

23 | 24 | x: 25 | y: 26 | 27 | 28 |

The value of x is {{data.x}} and y is {{data.y}}.

29 | 30 | 31 | 32 | 33 | 34 |

In this p5 program, we have disabled the draw() loop. So how is the animation being updated? A custom dataChanged() function is run every time that the data changes, which updates the canvas.

35 | 36 |

This is possible thanks to Vue's ability to 'watch' data. Data watchers are useful if you want to perform a computationally expensive operation in response to changing data. The alternative approach would be to check for a change in data on each frame of the draw() loop, which can be less efficient.

37 | 38 |

Source Code

39 | 40 |
41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /data-watching/sketch.js: -------------------------------------------------------------------------------- 1 | // this p5 sketch is written in instance mode 2 | // read more here: https://github.com/processing/p5.js/wiki/Global-and-instance-mode 3 | 4 | function sketch(parent) { // we pass the sketch data from the parent 5 | return function( p ) { // p could be any variable name 6 | 7 | // p5 sketch goes here 8 | 9 | p.setup = function() { 10 | let canvas = p.createCanvas(400, 200); 11 | canvas.parent(parent.$el); 12 | p.rectMode(p.CENTER); 13 | p.fill(255); 14 | p.background(0); 15 | p.rect(parent.data.x, parent.data.y, 50, 50); 16 | p.noLoop(); 17 | }; 18 | 19 | p.draw = function() { 20 | }; 21 | 22 | // this is a new function we've added to p5 23 | // it runs only if the data changes 24 | p.dataChanged = function(val, oldVal) { 25 | p.background(0); 26 | p.rect(val.x, val.y, 50, 50); 27 | }; 28 | 29 | }; 30 | } -------------------------------------------------------------------------------- /data-watching/vue-definitions.js: -------------------------------------------------------------------------------- 1 | // Defines a Vue Component 2 | 3 | Vue.component('p5', { 4 | 5 | template: '
', 6 | 7 | props: ['src','data'], 8 | 9 | methods: { 10 | // loadScript from https://stackoverflow.com/a/950146 11 | // loads the p5 javscript code from a file 12 | loadScript: function (url, callback) 13 | { 14 | // Adding the script tag to the head as suggested before 15 | var head = document.head; 16 | var script = document.createElement('script'); 17 | script.type = 'text/javascript'; 18 | script.src = url; 19 | 20 | // Then bind the event to the callback function. 21 | // There are several events for cross browser compatibility. 22 | script.onreadystatechange = callback; 23 | script.onload = callback; 24 | 25 | // Fire the loading 26 | head.appendChild(script); 27 | }, 28 | 29 | loadSketch: function() { 30 | this.myp5 = new p5(sketch(this)); 31 | } 32 | }, 33 | 34 | data: function() { 35 | return { 36 | myp5: {} 37 | } 38 | }, 39 | 40 | mounted() { 41 | this.loadScript(this.src, this.loadSketch); 42 | }, 43 | 44 | watch: { 45 | data: { 46 | handler: function(val, oldVal) { 47 | if(this.myp5.dataChanged) { 48 | this.myp5.dataChanged(val, oldVal); 49 | } 50 | }, 51 | deep: true 52 | } 53 | } 54 | 55 | }) 56 | 57 | // Sets up the main Vue instance 58 | 59 | var app = new Vue({ 60 | el: '#root', 61 | 62 | data: { 63 | 64 | // this data object stores variables that we want to share between components 65 | // this is not a good place to store data that doesn't need to be shared 66 | data: { 67 | x: 200, 68 | y: 100 69 | } 70 | 71 | } 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /interactive-narrative-2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 |
19 | 20 |

Scene 1

21 | 22 |

One Fish

23 | 24 | 25 |

Two Fish

26 | 27 | 28 |
29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 |
37 | 38 |

Scene 2

39 | 40 |

Red Fish

41 | 42 |

Blue Fish

43 | 44 | 45 | 46 |
47 | 48 | 49 | 50 |

Source Code

51 | 52 | 53 |
54 | 55 | 56 |
57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /interactive-narrative-2/p5-vue-component.js: -------------------------------------------------------------------------------- 1 | // Defines a Vue Component 2 | 3 | Vue.component('p5', { 4 | 5 | template: '
', 6 | 7 | props: ['src','data'], 8 | 9 | methods: { 10 | // loadScript from https://stackoverflow.com/a/950146 11 | // loads the p5 javscript code from a file 12 | loadScript: function (url, callback) 13 | { 14 | // Adding the script tag to the head as suggested before 15 | var head = document.head; 16 | var script = document.createElement('script'); 17 | script.type = 'text/javascript'; 18 | script.src = url; 19 | 20 | // Then bind the event to the callback function. 21 | // There are several events for cross browser compatibility. 22 | script.onreadystatechange = callback; 23 | script.onload = callback; 24 | 25 | // Fire the loading 26 | head.appendChild(script); 27 | }, 28 | 29 | loadSketch: function() { 30 | console.log(this.src, this.$el); 31 | this.myp5 = new p5(sketch(this), this.$el); 32 | } 33 | }, 34 | 35 | data: function() { 36 | return { 37 | myp5: {} 38 | } 39 | }, 40 | 41 | mounted() { 42 | this.loadScript(this.src, this.loadSketch); 43 | }, 44 | 45 | watch: { 46 | src: function() { 47 | this.$el.innerHTML = ''; 48 | this.loadScript(this.src, this.loadSketch); 49 | }, 50 | 51 | data: { 52 | handler: function(val, oldVal) { 53 | if(this.myp5.dataChanged) { 54 | this.myp5.dataChanged(val, oldVal); 55 | } 56 | }, 57 | deep: true 58 | } 59 | } 60 | 61 | }) -------------------------------------------------------------------------------- /interactive-narrative-2/scene-logic.js: -------------------------------------------------------------------------------- 1 | // Sets up the main Vue instance 2 | 3 | var app = new Vue({ 4 | el: '#story', 5 | 6 | data: { 7 | scene: 1, 8 | name: '' 9 | } 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /interactive-narrative-2/sketch1.js: -------------------------------------------------------------------------------- 1 | // this p5 sketch is written in instance mode 2 | // read more here: https://github.com/processing/p5.js/wiki/Global-and-instance-mode 3 | 4 | function sketch(parent) { // we pass the sketch data from the parent 5 | return function( p ) { // p could be any variable name 6 | 7 | p.setup = function() { 8 | let canvas = p.createCanvas(600, 600); 9 | canvas.parent(parent.$el); 10 | p.background('dodgerblue'); 11 | p.textAlign(p.CENTER, p.CENTER); 12 | p.textSize(100); 13 | p.text('🐟', p.width/2, p.height/2); 14 | p.noLoop(); 15 | }; 16 | 17 | p.draw = function() { 18 | }; 19 | 20 | // this is a new function we've added to p5 21 | // it runs only if the data changes 22 | p.dataChanged = function(val, oldVal) { 23 | // console.log('data changed'); 24 | // console.log('x: ', val.x, 'y: ', val.y); 25 | }; 26 | 27 | }; 28 | } -------------------------------------------------------------------------------- /interactive-narrative-2/sketch2.js: -------------------------------------------------------------------------------- 1 | // this p5 sketch is written in instance mode 2 | // read more here: https://github.com/processing/p5.js/wiki/Global-and-instance-mode 3 | 4 | function sketch(parent) { // we pass the sketch data from the parent 5 | return function( p ) { // p could be any variable name 6 | 7 | p.setup = function() { 8 | let canvas = p.createCanvas(600, 600); 9 | canvas.parent(parent.$el); 10 | p.background('dodgerblue'); 11 | p.textAlign(p.CENTER, p.CENTER); 12 | p.textSize(100); 13 | p.text('🐟🐟', p.width/2, p.height/2); 14 | p.noLoop(); 15 | }; 16 | 17 | p.draw = function() { 18 | }; 19 | 20 | // this is a new function we've added to p5 21 | // it runs only if the data changes 22 | p.dataChanged = function(val, oldVal) { 23 | // console.log('data changed'); 24 | // console.log('x: ', val.x, 'y: ', val.y); 25 | }; 26 | 27 | }; 28 | } -------------------------------------------------------------------------------- /interactive-narrative-2/sketch3.js: -------------------------------------------------------------------------------- 1 | // this p5 sketch is written in instance mode 2 | // read more here: https://github.com/processing/p5.js/wiki/Global-and-instance-mode 3 | 4 | function sketch(parent) { // we pass the sketch data from the parent 5 | return function( p ) { // p could be any variable name 6 | 7 | p.setup = function() { 8 | let canvas = p.createCanvas(600, 600); 9 | canvas.parent(parent.$el); 10 | p.background('red'); 11 | p.textAlign(p.CENTER, p.CENTER); 12 | p.textSize(200); 13 | p.text('🎏', p.width/2, p.height/2); 14 | p.noLoop(); 15 | }; 16 | 17 | p.draw = function() { 18 | }; 19 | 20 | // this is a new function we've added to p5 21 | // it runs only if the data changes 22 | p.dataChanged = function(val, oldVal) { 23 | // console.log('data changed'); 24 | // console.log('x: ', val.x, 'y: ', val.y); 25 | }; 26 | 27 | }; 28 | } -------------------------------------------------------------------------------- /interactive-narrative-2/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | canvas { 7 | display: block; 8 | } 9 | -------------------------------------------------------------------------------- /interactive-narrative/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 |
21 | 22 |

Scene {{scene}}

23 |

Hello traveller. What is your name?

24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 |
33 | 34 |

Scene {{scene}}

35 |

{{name}}, you are drifting in space.

36 | 37 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | 45 |
46 | 47 |

Scene {{scene}}

48 |

{{name}}, you are drifting towards Earth.

49 | 50 | 51 | 52 |
53 | 54 | 55 | 56 | 57 |
58 | 59 |

Scene {{scene}}

60 |

{{name}}, you are drifting towards the Sun.

61 | 62 | 63 | 64 |
65 | 66 | 67 | 68 |
69 | 70 |

Scene {{scene}}

71 |

{{name}}, you have arrived.

72 |

THE END.

73 | 74 | 75 | 76 |

Source Code

77 | 78 |
79 | 80 | 81 |
82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /interactive-narrative/p5-vue-component.js: -------------------------------------------------------------------------------- 1 | // Defines a Vue Component 2 | 3 | Vue.component('p5', { 4 | 5 | template: '
', 6 | 7 | props: ['src','data'], 8 | 9 | methods: { 10 | // loadScript from https://stackoverflow.com/a/950146 11 | // loads the p5 javscript code from a file 12 | loadScript: function (url, callback) 13 | { 14 | // Adding the script tag to the head as suggested before 15 | var head = document.head; 16 | var script = document.createElement('script'); 17 | script.type = 'text/javascript'; 18 | script.src = url; 19 | 20 | // Then bind the event to the callback function. 21 | // There are several events for cross browser compatibility. 22 | script.onreadystatechange = callback; 23 | script.onload = callback; 24 | 25 | // Fire the loading 26 | head.appendChild(script); 27 | }, 28 | 29 | loadSketch: function() { 30 | console.log(this.src, this.$el); 31 | this.myp5 = new p5(sketch(this), this.$el); 32 | } 33 | }, 34 | 35 | data: function() { 36 | return { 37 | myp5: {} 38 | } 39 | }, 40 | 41 | mounted() { 42 | this.loadScript(this.src, this.loadSketch); 43 | }, 44 | 45 | watch: { 46 | // this seems a bit hacky 47 | // when using v-if on a parent div, 48 | // vue doesn't replace the component but instead just changes the variables 49 | // its bound to. So we need to watch for the src variable to 50 | 51 | src: function() { 52 | this.$el.innerHTML = ''; 53 | this.loadScript(this.src, this.loadSketch); 54 | }, 55 | 56 | data: { 57 | handler: function(val, oldVal) { 58 | if(this.myp5.dataChanged) { 59 | this.myp5.dataChanged(val, oldVal); 60 | } 61 | }, 62 | deep: true 63 | } 64 | } 65 | 66 | }) -------------------------------------------------------------------------------- /interactive-narrative/scene-logic.js: -------------------------------------------------------------------------------- 1 | // Sets up the main Vue instance 2 | 3 | var app = new Vue({ 4 | el: '#story', 5 | 6 | data: { 7 | scene: 1, 8 | name: '' 9 | } 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /interactive-narrative/sketch.js: -------------------------------------------------------------------------------- 1 | // this p5 sketch is written in instance mode 2 | // read more here: https://github.com/processing/p5.js/wiki/Global-and-instance-mode 3 | 4 | function sketch(parent) { // we pass the sketch data from the parent 5 | return function( p ) { // p could be any variable name 6 | 7 | // p5 sketch goes here 8 | let size = 0; 9 | 10 | p.setup = function() { 11 | let canvas = p.createCanvas(p.windowWidth, p.windowHeight); 12 | canvas.parent(parent.$el); 13 | p.background(0); 14 | p.stroke(255); 15 | p.fill(255); 16 | for (var i = 0; i < 100; i++) { 17 | p.point(p.random(p.width), p.random(p.height)); 18 | } 19 | p.noStroke(); 20 | }; 21 | 22 | p.draw = function() { 23 | if (parent.data.scene == 3) { 24 | 25 | p.fill('blue'); 26 | p.circle(p.width/2, p.height/2, size); 27 | size += 1; 28 | 29 | } else if (parent.data.scene == 4) { 30 | 31 | p.fill('yellow'); 32 | p.circle(p.width/2, p.height/2, size); 33 | size += 1; 34 | 35 | } 36 | }; 37 | 38 | // this is a new function we've added to p5 39 | // it runs only if the data changes 40 | p.dataChanged = function(val, oldVal) { 41 | // console.log('data changed'); 42 | // console.log('x: ', val.x, 'y: ', val.y); 43 | }; 44 | 45 | }; 46 | } -------------------------------------------------------------------------------- /interactive-narrative/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | color: white; 5 | text-align: center; 6 | } 7 | 8 | canvas { 9 | display: block; 10 | position: absolute; 11 | left: 0px; 12 | top: 0px; 13 | z-index: -1; 14 | } 15 | 16 | p { 17 | font-size: 1.5rem; 18 | } -------------------------------------------------------------------------------- /one-way-binding/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |

One Way Binding Example

15 | 16 |

Slide me!

17 | 18 | 19 | 20 | 21 | 22 |

Edit me!

23 | 24 | x: 25 | y: 26 | 27 | 28 |

The value of x is {{data.x}} and y is {{data.y}}.

29 | 30 | 31 | 32 | 33 | 34 |

Source Code

35 | 36 |
37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /one-way-binding/sketch1.js: -------------------------------------------------------------------------------- 1 | // this p5 sketch is written in instance mode 2 | // read more here: https://github.com/processing/p5.js/wiki/Global-and-instance-mode 3 | 4 | function sketch(parent) { // we pass the sketch data from the parent 5 | return function( p ) { // p could be any variable name 6 | 7 | // p5 sketch goes here 8 | 9 | p.setup = function() { 10 | let canvas = p.createCanvas(400, 200); 11 | canvas.parent(parent.$el); 12 | p.rectMode(p.CENTER); 13 | }; 14 | 15 | p.draw = function() { 16 | p.background(0); 17 | p.fill(255); 18 | p.rect(parent.data.x, parent.data.y, 50, 50); 19 | }; 20 | 21 | // this is a new function we've added to p5 22 | // it runs only if the data changes 23 | p.dataChanged = function(val, oldVal) { 24 | // console.log('data changed'); 25 | // console.log('x: ', val.x, 'y: ', val.y); 26 | }; 27 | 28 | }; 29 | } -------------------------------------------------------------------------------- /one-way-binding/sketch2.js: -------------------------------------------------------------------------------- 1 | // this p5 sketch is written in instance mode 2 | // read more here: https://github.com/processing/p5.js/wiki/Global-and-instance-mode 3 | 4 | function sketch(parent) { // we pass the sketch data from the parent 5 | return function( p ) { // p could be any variable name 6 | // p5 sketch goes here 7 | 8 | p.setup = function() { 9 | let canvas = p.createCanvas(400, 200); 10 | canvas.parent(parent.$el); 11 | }; 12 | 13 | p.draw = function() { 14 | p.background(0); 15 | p.fill(255); 16 | p.ellipse(p.width - parent.data.x, p.height - parent.data.y,50,50); 17 | }; 18 | 19 | // this is a new function we've added to p5 20 | // it runs only if the data changes 21 | p.dataChanged = function(val, oldVal) { 22 | // console.log('data changed'); 23 | // console.log('x: ', val.x, 'y: ', val.y); 24 | }; 25 | 26 | }; 27 | } -------------------------------------------------------------------------------- /one-way-binding/vue-definitions.js: -------------------------------------------------------------------------------- 1 | // Defines a Vue Component 2 | 3 | Vue.component('p5', { 4 | 5 | template: '
', 6 | 7 | props: ['src','data'], 8 | 9 | methods: { 10 | // loadScript from https://stackoverflow.com/a/950146 11 | // loads the p5 javscript code from a file 12 | loadScript: function (url, callback) 13 | { 14 | // Adding the script tag to the head as suggested before 15 | var head = document.head; 16 | var script = document.createElement('script'); 17 | script.type = 'text/javascript'; 18 | script.src = url; 19 | 20 | // Then bind the event to the callback function. 21 | // There are several events for cross browser compatibility. 22 | script.onreadystatechange = callback; 23 | script.onload = callback; 24 | 25 | // Fire the loading 26 | head.appendChild(script); 27 | }, 28 | 29 | loadSketch: function() { 30 | this.myp5 = new p5(sketch(this)); 31 | } 32 | }, 33 | 34 | data: function() { 35 | return { 36 | myp5: {} 37 | } 38 | }, 39 | 40 | mounted() { 41 | this.loadScript(this.src, this.loadSketch); 42 | }, 43 | 44 | watch: { 45 | data: { 46 | handler: function(val, oldVal) { 47 | if(this.myp5.dataChanged) { 48 | this.myp5.dataChanged(val, oldVal); 49 | } 50 | }, 51 | deep: true 52 | } 53 | } 54 | 55 | }) 56 | 57 | // Sets up the main Vue instance 58 | 59 | var app = new Vue({ 60 | el: '#root', 61 | 62 | data: { 63 | 64 | // this data object stores variables that we want to share between components 65 | // this is not a good place to store data that doesn't need to be shared 66 | data: { 67 | x: 200, 68 | y: 100 69 | } 70 | 71 | } 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /two-way-binding/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |

Two Way Binding Example

15 | 16 |

Slide me!

17 | 18 | 19 | 20 | 21 | 22 |

Edit me!

23 | 24 | x: 25 | y: 26 | 27 | 28 |

The value of x is {{data.x}} and y is {{data.y}}.

29 | 30 | 31 | 32 | 33 | 34 |

Source Code

35 | 36 |
37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /two-way-binding/sketch1.js: -------------------------------------------------------------------------------- 1 | // this p5 sketch is written in instance mode 2 | // read more here: https://github.com/processing/p5.js/wiki/Global-and-instance-mode 3 | 4 | function sketch(parent) { // we pass the sketch data from the parent 5 | return function( p ) { // p could be any variable name 6 | 7 | // p5 sketch goes here 8 | 9 | p.setup = function() { 10 | let canvas = p.createCanvas(400, 200); 11 | canvas.parent(parent.$el); 12 | p.rectMode(p.CENTER); 13 | }; 14 | 15 | p.draw = function() { 16 | p.background(0); 17 | p.fill(255); 18 | p.rect(parent.data.x, parent.data.y, 50, 50); 19 | p.fill(0); 20 | p.text('Drag\nme!', parent.data.x - 12, parent.data.y - 5); 21 | }; 22 | 23 | p.mouseDragged = function() { 24 | // check that input came from within this canvas 25 | if (0 < p.mouseX && p.mouseX < p.width && 0 < p.mouseY && p.mouseY < p.height) 26 | { 27 | parent.$emit('update:x', p.round(p.mouseX) ); 28 | parent.$emit('update:y', p.round(p.mouseY) ); 29 | } 30 | } 31 | 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /two-way-binding/sketch2.js: -------------------------------------------------------------------------------- 1 | // this p5 sketch is written in instance mode 2 | // read more here: https://github.com/processing/p5.js/wiki/Global-and-instance-mode 3 | 4 | function sketch(parent) { // we pass the sketch data from the parent 5 | return function( p ) { // p could be any variable name 6 | // p5 sketch goes here 7 | 8 | p.setup = function() { 9 | let canvas = p.createCanvas(400, 200); 10 | canvas.parent(parent.$el); 11 | }; 12 | 13 | p.draw = function() { 14 | p.background(0); 15 | p.fill(255); 16 | p.ellipse(p.width - parent.data.x, p.height - parent.data.y, 50, 50); 17 | p.fill(0); 18 | p.text('Drag\nme!', p.width - parent.data.x - 12, p.height - parent.data.y - 5); 19 | }; 20 | 21 | p.mouseDragged = function() { 22 | // check that input came from within this canvas 23 | if (0 < p.mouseX && p.mouseX < p.width && 0 < p.mouseY && p.mouseY < p.height) 24 | { 25 | parent.$emit('update:x', p.round(p.width - p.mouseX) ); 26 | parent.$emit('update:y', p.round(p.height - p.mouseY) ); 27 | } 28 | } 29 | 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /two-way-binding/vue-definitions.js: -------------------------------------------------------------------------------- 1 | // Defines a Vue Component 2 | 3 | Vue.component('p5', { 4 | 5 | template: '
', 6 | 7 | props: ['src','data'], 8 | 9 | methods: { 10 | // loadScript from https://stackoverflow.com/a/950146 11 | // loads the p5 javscript code from a file 12 | loadScript: function (url, callback) 13 | { 14 | // Adding the script tag to the head as suggested before 15 | var head = document.head; 16 | var script = document.createElement('script'); 17 | script.type = 'text/javascript'; 18 | script.src = url; 19 | 20 | // Then bind the event to the callback function. 21 | // There are several events for cross browser compatibility. 22 | script.onreadystatechange = callback; 23 | script.onload = callback; 24 | 25 | // Fire the loading 26 | head.appendChild(script); 27 | }, 28 | 29 | loadSketch: function() { 30 | this.myp5 = new p5(sketch(this)); 31 | } 32 | }, 33 | 34 | data: function() { 35 | return { 36 | myp5: {} 37 | } 38 | }, 39 | 40 | mounted() { 41 | this.loadScript(this.src, this.loadSketch); 42 | }, 43 | 44 | watch: { 45 | data: { 46 | handler: function(val, oldVal) { 47 | if(this.myp5.dataChanged) { 48 | this.myp5.dataChanged(val, oldVal); 49 | } 50 | }, 51 | deep: true 52 | } 53 | } 54 | 55 | }) 56 | 57 | // Sets up the main Vue instance 58 | 59 | var app = new Vue({ 60 | el: '#root', 61 | 62 | data: { 63 | 64 | // this data object stores variables that we want to share between components 65 | // this is not a good place to store data that doesn't need to be shared 66 | data: { 67 | x: 200, 68 | y: 100 69 | } 70 | 71 | } 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /whale-story/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 |
27 |

Whale Story

28 |

A playable narrative built with p5.js and Vue.js

29 | 30 |
31 | 32 |
33 | 34 | 35 | 36 | 37 |
38 | 39 |

Hello. You have been turned into a whale.

40 | 41 |
42 |
43 |
44 | 45 |

What kind of whale would you rather be?

46 | 47 |
48 |

🐋 a humpback whale

49 |

🐳 a blue whale

50 |
51 | 52 | 53 | 54 |
55 | 56 | 57 | 58 |
59 | 60 |

Congratulations! You are now a {{sketch.whale}}.

61 | 62 |

A swarm of fish approach.

63 | 64 |

In the background, you hear a faint whale song.

65 | 66 |

oooooOOOOOOOOooooooo

67 | 68 |
    69 |
  • 70 |
  • 71 |
72 | 73 |
74 | 75 | 76 | 77 |
78 | 79 |

Mmmmmm. That was delicious.

80 | 81 |

You have eaten {{sketch.fishEaten}} fish.

82 | 83 |
    84 |
  • 85 |
  • 86 |
87 | 88 |
89 | 90 | 91 | 92 |
93 | 94 |

Uh-oh.

95 |

One of the fish you ate was a poisonous puffer fish.

96 |

You start to feel dizzy.

97 | 98 |
    99 |
  • 100 |
  • 101 |
102 | 103 |
104 | 105 | 106 | 107 |
108 | 109 |

You arrive at the surface and spout water through your blowhole.

110 | 111 |

People in a boat see you and wave.

112 | 113 |

You feel much better.

114 | 115 | 116 | 117 |
118 | 119 | 120 | 121 |
122 | 123 |

The whale song grows louder.

124 |

ooooooooOOOOOOOOooooooo

125 | 126 | 127 | 128 |
129 | 130 | 131 | 132 | 133 |
134 | 135 |

The whale song grows even louder.

136 |

ooooooooOOOOOOOOoooooooOOOOOOOOoooooooooo

137 | 138 | 139 | 140 |
141 | 142 | 143 | 144 | 145 |
146 | 147 |

The whale song grows louder still.

148 |

ooooooooOOOOOOOOoooooooOOOOOOOOooooooooooOOOOOOOOoooooooooo

149 | 150 | 151 | 152 |
153 | 154 | 155 | 156 | 157 |
158 | 159 |

The sun has set. The ocean turns dark.

160 | 161 |

You are somewhat lost and no longer hear the song.

162 | 163 |
    164 |
  • 165 |
  • 166 |
167 | 168 |
169 | 170 | 171 | 172 |
173 | 174 |

It is now morning.

175 | 176 |

Another whale hears your whale song!!

177 | 178 |
    179 |
  • 180 |
  • 181 |
182 | 183 |
184 | 185 | 186 | 187 |
188 | 189 |

You approach the other whale.

190 | 191 |

"Hi!", says the other whale.

192 | 193 |

"Would you like to follow me to a secret underwater garden?"

194 | 195 |
    196 |
  • 197 |
198 | 199 |
200 | 201 | 202 | 203 |
204 | 205 |

The other whale approaches.

206 | 207 |

"Hi!", says the other whale.

208 | 209 |

"Would you like to follow me to a secret underwater garden?"

210 | 211 |
    212 |
  • 213 |
214 | 215 |
216 | 217 | 218 | 219 |
220 | 221 |

Together, you frolick in the garden.

222 | 223 |

THE END

224 | 225 |
    226 |
  • 227 |
  • 228 |
229 | 230 |
231 | 232 | 233 | 234 | 235 | 236 |
237 | 238 | 239 | 240 |

This is an example of an interactive narrative.

241 | 242 |

You can find the source code here.

243 | 244 |

To remix:

245 |
    246 |
  • Download the code and run it on your own computer.

  • 247 |
  • Modify the story

  • 248 |
  • Change the animations

  • 249 |
  • Publish your own story

  • 250 |
251 | 252 |

This work is released into the public domain, so you can do what you like with it, and use it however you want.

253 | 254 |

Have fun!

255 | 256 |
257 | 258 | 259 |
260 | 261 | 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /whale-story/normalize.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, 8 | a, abbr, acronym, address, big, cite, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /whale-story/p5-vue-component.js: -------------------------------------------------------------------------------- 1 | // Defines a Vue Component 2 | 3 | Vue.component('p5', { 4 | 5 | template: '
', 6 | 7 | props: ['src','data'], 8 | 9 | methods: { 10 | // loadScript from https://stackoverflow.com/a/950146 11 | // loads the p5 javscript code from a file 12 | loadScript: function (url, callback) 13 | { 14 | // Adding the script tag to the head as suggested before 15 | var head = document.head; 16 | var script = document.createElement('script'); 17 | script.type = 'text/javascript'; 18 | script.src = url; 19 | 20 | // Then bind the event to the callback function. 21 | // There are several events for cross browser compatibility. 22 | script.onreadystatechange = callback; 23 | script.onload = callback; 24 | 25 | // Fire the loading 26 | head.appendChild(script); 27 | }, 28 | 29 | loadSketch: function() { 30 | this.myp5 = new p5(sketch(this), this.$el); 31 | } 32 | }, 33 | 34 | data: function() { 35 | return { 36 | myp5: {} 37 | } 38 | }, 39 | 40 | mounted() { 41 | this.loadScript(this.src, this.loadSketch); 42 | }, 43 | 44 | watch: { 45 | // this seems a bit hacky 46 | // when using v-if on a parent div, 47 | // vue doesn't replace the component but instead just changes the variables 48 | // its bound to. So we need to watch for the src variable to 49 | 50 | src: function() { 51 | this.$el.innerHTML = ''; 52 | this.loadScript(this.src, this.loadSketch); 53 | }, 54 | 55 | data: { 56 | handler: function(val, oldVal) { 57 | if(this.myp5.dataChanged) { 58 | this.myp5.dataChanged(val, oldVal); 59 | } 60 | }, 61 | deep: true 62 | } 63 | } 64 | 65 | }) -------------------------------------------------------------------------------- /whale-story/scene-logic.js: -------------------------------------------------------------------------------- 1 | // Sets up the main Vue instance 2 | 3 | var app = new Vue({ 4 | el: '#story', 5 | 6 | // scene logic functions 7 | methods: { 8 | eatFish: function() { 9 | if(Math.random() <= 0.25) 10 | { 11 | this.sketch.scene = 4; 12 | } 13 | else { 14 | this.sketch.fishEaten += Math.round(Math.random()*50 + 50); 15 | this.sketch.scene = 3; 16 | } 17 | } 18 | }, 19 | 20 | data: { 21 | 22 | // data that we want to access in html & the p5 sketch doesn't need to see 23 | 24 | 25 | // data that we want the p5 sketch to be able to access 26 | sketch: { 27 | scene: 0, 28 | fishEaten: 0, 29 | whale: '', 30 | } 31 | 32 | } 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /whale-story/sketch.js: -------------------------------------------------------------------------------- 1 | // this p5 sketch is written in instance mode 2 | // read more here: https://github.com/processing/p5.js/wiki/Global-and-instance-mode 3 | 4 | function sketch(parent) { // we pass the sketch data from the parent 5 | return function( p ) { // p could be any variable name 6 | 7 | // p5 sketch goes here 8 | let time = 0; 9 | let sceneTime = 0; 10 | let scene = 0; 11 | 12 | let fishPos, whalePos, boatPos; 13 | let whaleX, whaleY; 14 | let whaleSize; 15 | 16 | let finalScene = false; 17 | let finalSceneStartTime; 18 | 19 | let x, y; 20 | 21 | p.setup = function() { 22 | let canvas = p.createCanvas(p.windowWidth, p.windowHeight); 23 | canvas.parent(parent.$el); 24 | p.noStroke(); 25 | whaleSize = p.height / 2.5; 26 | p.textSize(whaleSize); 27 | p.textAlign(p.CENTER, p.CENTER); 28 | }; 29 | 30 | p.draw = function() { 31 | 32 | // DRAW WAVES 33 | 34 | if (!finalScene) { 35 | 36 | for(let y = -25; y < p.height; y += 50) { 37 | let blueVal = p.map(y, -25, p.height, 230, 50); 38 | p.fill(50, 50, blueVal); 39 | 40 | p.beginShape(); 41 | p.vertex(0, p.height); 42 | for (let x=0; x < p.width; x++) { 43 | let wavelength = 70; 44 | let k = 2 * Math.PI / wavelength; 45 | let v = 0.1 * y/p.height; 46 | let amplitude = 20; 47 | let verticalOffset = y + 10 * p.sin(0.1 * (y + 0.01 * time)); 48 | p.vertex(x, amplitude * p.sin(k * (x + v * time)) + verticalOffset); 49 | } 50 | p.vertex(p.width, p.height); 51 | p.endShape(); 52 | } 53 | 54 | } 55 | 56 | 57 | if (parent.data.scene == 2) { 58 | p.textSize(whaleSize); 59 | p.text(parent.data.whale, 0.8 * p.width, 0.8 * p.height); 60 | 61 | fishPos = p.map(sceneTime, 0, 2000, 0, 0.25 * p.width, true); 62 | 63 | p.textSize(whaleSize/2); 64 | p.text('🐠🐟', fishPos, 0.7 * p.height); 65 | p.text('🐟🐟', fishPos, 0.8 * p.height); 66 | p.text('🐟🐠', fishPos, 0.9 * p.height); 67 | 68 | whaleX = 0.8 * p.width; 69 | whaleY = 0.8 * p.height; 70 | 71 | } 72 | 73 | else if (parent.data.scene == 3) { 74 | 75 | p.textSize(whaleSize/2); 76 | p.text('🐠🐟', fishPos, 0.7 * p.height); 77 | p.text('🐟🐟', fishPos, 0.8 * p.height); 78 | p.text('🐟🐠', fishPos, 0.9 * p.height); 79 | 80 | whalePos = p.map(sceneTime, 0, 1200, 0.8 * p.width, fishPos, true); 81 | 82 | p.textSize(whaleSize); 83 | p.text(parent.data.whale, whalePos, 0.8 * p.height); 84 | 85 | } 86 | 87 | else if (parent.data.scene == 4) { 88 | 89 | p.textSize(whaleSize/2); 90 | p.text('🐠🐟', fishPos, 0.7 * p.height); 91 | p.text('🐡🐟', fishPos, 0.8 * p.height); 92 | p.text('🐟🐠', fishPos, 0.9 * p.height); 93 | 94 | whalePos = p.map(sceneTime, 0, 1200, 0.8 * p.width, fishPos, true); 95 | 96 | p.textSize(whaleSize); 97 | 98 | if (sceneTime < 1200) { 99 | p.text(parent.data.whale, whalePos, 0.8 * p.height); 100 | } 101 | 102 | else { 103 | // dizzy animation 104 | whaleX = whalePos + whaleSize/2 * p.sin(0.002 * sceneTime); 105 | whaleY = 0.8 * p.height + whaleSize/2 * p.sin(0.004 * sceneTime); 106 | 107 | p.text(parent.data.whale, whaleX, whaleY); 108 | 109 | } 110 | } 111 | 112 | else if (parent.data.scene == 5) { 113 | 114 | let prevWhaleY; 115 | if(!prevWhaleY) {prevWhaleY = whaleY;} 116 | // rise to the surface 117 | p.textSize(whaleSize); 118 | whaleY = prevWhaleY + p.map(sceneTime, 0, 1000, 0, -prevWhaleY + whaleSize/2, true); 119 | p.text(parent.data.whale, whaleX, whaleY); 120 | 121 | // People in a boat see you and wave. 122 | if (sceneTime > 500) { 123 | boatPos = p.map(sceneTime, 500, 1500, p.width + whaleSize, 0.8 * p.width, true); 124 | p.text('⛵', boatPos, whaleY); 125 | } 126 | 127 | 128 | } 129 | 130 | else if (parent.data.scene == 6) { 131 | 132 | // The whale song grows louder. 133 | 134 | let prevWhaleY; 135 | if(!prevWhaleY) {prevWhaleY = whaleY;} 136 | 137 | // if there was a boat, move it away 138 | if (boatPos) { 139 | 140 | boatPos = p.map(sceneTime, 0, 1000, 0.8 * p.width, 1.2 * p.width, true); 141 | p.text('⛵', boatPos, prevWhaleY); 142 | } 143 | 144 | x = p.map(sceneTime, 0, 1000, whaleX, 0.8 * p.width, true); 145 | y = p.map(sceneTime, 0, 1000, whaleY, 0.8 * p.height, true); 146 | 147 | p.textSize(whaleSize); 148 | p.text(parent.data.whale, x, y); 149 | 150 | } 151 | 152 | else if (parent.data.scene == 7) { 153 | 154 | x = p.map(sceneTime, 0, 1000, 0.8 * p.width, -0.2 * p.width, true); 155 | y = 0.8 * p.height; 156 | 157 | if (x < 0) {x += p.width;} 158 | p.textSize(whaleSize); 159 | p.text(parent.data.whale, x, y); 160 | } 161 | 162 | else if (parent.data.scene == 8) { 163 | 164 | x = p.map(sceneTime, 0, 1000, 0.8 * p.width, -0.2 * p.width, true); 165 | y = 0.8 * p.height; 166 | 167 | if (x < 0) {x += p.width;} 168 | p.textSize(whaleSize); 169 | p.text(parent.data.whale, x, y); 170 | } 171 | 172 | else if (parent.data.scene == 9) { 173 | 174 | // The sun has set. The ocean turns dark. 175 | 176 | let fade = p.map(sceneTime, 0, 2000, 0, 255, true); 177 | 178 | p.background(0, 0, 0, fade); 179 | 180 | p.textSize(whaleSize); 181 | p.text(parent.data.whale, 0.8 * p.width, 0.8 * p.height); 182 | 183 | } 184 | 185 | else if (parent.data.scene == 10) { 186 | 187 | // Another whale hears your whale song.. 188 | 189 | let fade = p.map(sceneTime, 0, 1000, 255, 0, true); 190 | 191 | p.background(0, 0, 0, fade); 192 | 193 | p.textSize(whaleSize); 194 | p.text(parent.data.whale, 0.8 * p.width, 0.8 * p.height); 195 | 196 | if (sceneTime > 1000) { 197 | x = p.map(sceneTime, 1000, 2000, -0.2 * p.width, 0.2 * p.width, true); 198 | p.text(parent.data.whale, x, 0.8 * p.height); 199 | } 200 | 201 | } 202 | 203 | else if (parent.data.scene == 11) { 204 | 205 | // You approach the other whale 206 | 207 | x = p.map(sceneTime, 0, 1000, 0.8 * p.width, 0.3 * p.width, true); 208 | 209 | p.text(parent.data.whale, 0.2 * p.width, 0.8 * p.height); 210 | 211 | p.textSize(whaleSize); 212 | p.text(parent.data.whale, x, 0.8 * p.height); 213 | 214 | whaleX = x; 215 | whaleY = 0.8 * p.height; 216 | 217 | friendWhaleX = 0.2 * p.width; 218 | friendWhaleY = 0.8 * p.height; 219 | } 220 | 221 | else if (parent.data.scene == 12) { 222 | 223 | // The other whale approaches. 224 | 225 | x = p.map(sceneTime, 0, 1000, 0.2 * p.width, 0.7 * p.width, true); 226 | 227 | p.textSize(whaleSize); 228 | p.text(parent.data.whale, 0.8 * p.width, 0.8 * p.height); 229 | p.text(parent.data.whale, x, 0.8 * p.height); 230 | 231 | whaleX = 0.8 * p.width; 232 | whaleY = 0.8 * p.height; 233 | 234 | friendWhaleX = x; 235 | friendWhaleY = 0.8 * p.height; 236 | 237 | } 238 | 239 | else if (parent.data.scene == 13) { 240 | 241 | // Together, you frolick in the garden. 242 | 243 | let prevLeftWhaleX, prevLeftWhaleY, whaleOrder, whaleSpacing; 244 | if(!prevLeftWhaleX) { 245 | prevLeftWhaleX = p.min(whaleX, friendWhaleX); 246 | prevLeftWhaleY = p.min(whaleY, friendWhaleY); 247 | whaleSpacing = p.abs(whaleX - friendWhaleX); 248 | } 249 | 250 | if (sceneTime < 1000) { 251 | x = p.map(sceneTime, 0, 1000, prevLeftWhaleX, 0.5 * p.width, true); 252 | y = p.map(sceneTime, 0, 1000, prevLeftWhaleY, 0.5 * p.height, true); 253 | 254 | p.textSize(whaleSize); 255 | p.text(parent.data.whale, x - whaleSpacing/2, y); 256 | p.text(parent.data.whale, x + whaleSpacing/2, y); 257 | } 258 | 259 | else if (sceneTime < 2500) { 260 | let fade = p.map(sceneTime, 1000, 2000, 0, 255); 261 | p.background(0, 0, 0, fade); 262 | 263 | p.textSize(whaleSize); 264 | p.text(parent.data.whale, x - whaleSpacing/2, y); 265 | p.text(parent.data.whale, x + whaleSpacing/2, y); 266 | } 267 | 268 | else { 269 | finalScene = true; 270 | // underwater garden 271 | p.background(10, 10); 272 | p.fill(40, 200, 40); 273 | 274 | if(!finalSceneStartTime) { 275 | finalSceneStartTime = sceneTime; 276 | } 277 | 278 | 279 | for (let x0 = 0; x0 <= p.width; x0 = x0 + 30) { 280 | for (let y0 = 0; y0 <= p.height; y0 = y0 + 30) { 281 | // starting point of each circle depends on mouse position 282 | let xAngle = 3.5 * p.PI; 283 | let yAngle = 3.5 * p.PI; 284 | // and also varies based on the particle's location 285 | let angle = xAngle * (x0 / p.width) + yAngle * (y0 / p.height); 286 | 287 | // each particle moves in a circle 288 | let myX = x0 + 20 * p.cos(2 * p.PI * sceneTime/5000 + angle); 289 | let myY = y0 + 20 * p.sin(2 * p.PI * sceneTime/5000 + angle); 290 | 291 | p.ellipse(myX, myY, 10); // draw particle 292 | } 293 | } 294 | 295 | 296 | let x1 = x + 2 * whaleSize * p.sin(0.001 * (sceneTime - finalSceneStartTime)); 297 | let y1 = y + whaleSize * p.sin(0.002 * (sceneTime - finalSceneStartTime)); 298 | 299 | p.textSize(whaleSize); 300 | p.text(parent.data.whale, x1, y1); 301 | p.text(parent.data.whale, x1 + whaleSpacing, y1); 302 | 303 | } 304 | 305 | } 306 | 307 | time += p.deltaTime; 308 | sceneTime += p.deltaTime; 309 | } 310 | 311 | 312 | // this is a new function we've added to p5 313 | // it runs only if the data changes 314 | p.dataChanged = function(val, oldVal) { 315 | scene = val.scene; 316 | sceneTime = 0; 317 | }; 318 | 319 | }; 320 | } -------------------------------------------------------------------------------- /whale-story/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | padding: 0; 3 | margin: 0; 4 | color: rgba(240,240,240); 5 | text-align: center; 6 | font-family: 'Fauna One', serif; 7 | } 8 | 9 | body { 10 | margin: 20px; 11 | } 12 | 13 | canvas { 14 | display: block; 15 | position: absolute; 16 | left: 0px; 17 | top: 0px; 18 | z-index: -1; 19 | } 20 | 21 | a { 22 | color: white; 23 | } 24 | 25 | p { 26 | font-size: 2rem; 27 | } 28 | 29 | h1 { 30 | font-family: 'Playfair Display', serif; 31 | margin-top:20px; 32 | text-align:center; 33 | font-weight:100; 34 | font-size:7rem; 35 | } 36 | 37 | h2 { 38 | margin-top:30px; 39 | font-weight:700; 40 | font-size: 1.8rem; 41 | padding-bottom:20px; 42 | 43 | } 44 | 45 | h3 { 46 | text-align:center; 47 | font-weight:400; 48 | font-size:1.2rem; 49 | } 50 | 51 | #title { 52 | padding-bottom:20px; 53 | margin-bottom:20px; 54 | } 55 | 56 | #title h1 { 57 | margin-bottom:10px; 58 | } 59 | 60 | h1, h2, h3 { 61 | line-height: 1.1; 62 | color: '#222'; 63 | } 64 | button { 65 | margin-top: 1rem; 66 | background: #216583; 67 | color: rgba(220,220,220); 68 | font-family: 'Fauna One', serif; 69 | border-color: black; 70 | font-size: 1.5rem; padding: 0.5rem; text-decoration: none; 71 | } 72 | 73 | ul { 74 | list-style-type: none 75 | } 76 | -------------------------------------------------------------------------------- /whale-story/whalestory.txt: -------------------------------------------------------------------------------- 1 | Whale Story 2 | 3 | 4 | * Scene 1: 5 | 6 | Hello. You have been turned into a whale. 7 | 8 | What kind of whale would you rather be? 9 | 10 | - a humpback whale 🐋 11 | - a blue whale 🐳 12 | 13 | [Next] 14 | 15 | 16 | * Scene 2: 17 | 18 | Congratulations! You are now a [whale type]. 19 | 20 | A swarm of fish swim by. 21 | In the background, you hear a faint whale song. (oooooOOOOOOOOooooooo) 22 | 23 | What would you like to do? 24 | 25 | - Eat the fish (50% --> poison fish, 50% --> eat more fish) 26 | - Swim towards the whale song 27 | 28 | 29 | * Scene 3: 30 | 31 | choice: eat the fish 32 | 33 | Mmmm. That was delicious. 34 | 35 | - Eat more fish (50% --> poison fish, 50% --> eat more fish) 36 | - Swim towards the whale song 37 | 38 | 39 | * Scene 4: 40 | 41 | choice: Eat more Fish 42 | 43 | Uh-oh. One of the fish you ate was a poisonous puffer fish. You start to feel dizzy. 44 | 45 | - Keep swimming forwards 46 | - Go up for some air 47 | 48 | 49 | * Scene 5 50 | 51 | choice: Go up for some air 52 | 53 | You arrive at the surface and spout water through your blowhole. 54 | 55 | People in a boat see you and wave. 56 | 57 | You feel much better. 58 | 59 | - Go back down 60 | 61 | 62 | * Scene 6: 63 | 64 | choice: go back down | keep swimming forwards | swim towards the whale song 65 | 66 | ooooooooOOOOOOOOoooooooOOOOOOOOoooooooooo 67 | 68 | 69 | - Keep swimming 70 | 71 | 72 | * Scene 7: 73 | 74 | choice: keep swimming 75 | 76 | The sun has set. It is now super dark. You are somewhat lost and no longer hear any whale song. 77 | 78 | - Sing a whale song 79 | - Wait 80 | 81 | 82 | * Scene 8 83 | 84 | choice: sing a whale song 85 | 86 | Another whale hears your whale song. 87 | 88 | - Approach the other whale. 89 | - I'm shy. I'll just stay here for a bit. 90 | 91 | 92 | * Scene 9 93 | 94 | choice: approach the other whale 95 | 96 | Both whales meet and then swim away into the sunset. 97 | 98 | THE END. 99 | 100 | [Play again.] 101 | 102 | [How was this created?] 103 | 104 | 105 | * Scene 10 106 | 107 | choice: I'll wait for the other whale to approach me. 108 | 109 | Both whales meet and then swim away into the sunset. 110 | 111 | THE END. 112 | 113 | [Play again. -- reloads the page] 114 | 115 | [How was this created?] 116 | 117 | 118 | * Credits 119 | 120 | Option: How was this created? 121 | 122 | 123 | 124 | This is an example of an interactive narrative. The animations were created using p5.js, a beginner-friendly creative coding library. The interaction was handled using Vue.js, a library that helps you add interaction to web pages. 125 | 126 | You can find the source code for this example here. 127 | 128 | How to remix this: 129 | - Download the code and run it on your own computer. 130 | - Modify the story 131 | - Change the animations 132 | - Publish your own story 133 | 134 | This work is released into the public domain, so you can do what you like with it, and use it however you want. 135 | 136 | Happy Tinkering! 137 | 138 | 139 | 140 | --------------------------------------------------------------------------------