mirror of
				https://github.com/bra1n/townsquare.git
				synced 2025-10-21 16:55:12 +00:00 
			
		
		
		
	add screenshot functionality
This commit is contained in:
		
							parent
							
								
									429d9845b1
								
							
						
					
					
						commit
						be44e977eb
					
				
					 6 changed files with 154 additions and 15 deletions
				
			
		|  | @ -7,12 +7,4 @@ It is supposed to aid storytellers and allow them to quickly set up and capture | ||||||
| 
 | 
 | ||||||
| [You can try it online!](https://bra1n.github.io/townsquare) | [You can try it online!](https://bra1n.github.io/townsquare) | ||||||
| 
 | 
 | ||||||
| **Todo:** |  | ||||||
| - add night sheet data to roles.json |  | ||||||
| - add night sheet view to Grimoire |  | ||||||
| - add global reminder space |  | ||||||
| - add LICENSE and finish README (shortcuts) |  | ||||||
| - (maybe) switch to vectorized SVG token icons |  | ||||||
| - allow using custom scripts |  | ||||||
| 
 |  | ||||||
| WORK IN PROGRESS | WORK IN PROGRESS | ||||||
|  |  | ||||||
							
								
								
									
										34
									
								
								src/App.vue
									
										
									
									
									
								
							
							
						
						
									
										34
									
								
								src/App.vue
									
										
									
									
									
								
							|  | @ -6,6 +6,7 @@ | ||||||
|       :players="players" |       :players="players" | ||||||
|       :roles="roles" |       :roles="roles" | ||||||
|       :zoom="zoom" |       :zoom="zoom" | ||||||
|  |       @screenshot="takeScreenshot" | ||||||
|     ></TownSquare> |     ></TownSquare> | ||||||
| 
 | 
 | ||||||
|     <Modal |     <Modal | ||||||
|  | @ -34,7 +35,17 @@ | ||||||
|       @close="isRoleModalOpen = false" |       @close="isRoleModalOpen = false" | ||||||
|     ></RoleSelectionModal> |     ></RoleSelectionModal> | ||||||
| 
 | 
 | ||||||
|  |     <Screenshot | ||||||
|  |       ref="screenshot" | ||||||
|  |       @success="isScreenshotSuccess = true" | ||||||
|  |     ></Screenshot> | ||||||
|  | 
 | ||||||
|     <div class="controls"> |     <div class="controls"> | ||||||
|  |       <font-awesome-icon | ||||||
|  |         icon="camera" | ||||||
|  |         @click="takeScreenshot()" | ||||||
|  |         v-bind:class="{ success: isScreenshotSuccess }" | ||||||
|  |       /> | ||||||
|       <font-awesome-icon icon="cogs" @click="isControlOpen = !isControlOpen" /> |       <font-awesome-icon icon="cogs" @click="isControlOpen = !isControlOpen" /> | ||||||
|       <ul v-if="isControlOpen"> |       <ul v-if="isControlOpen"> | ||||||
|         <li @click="togglePublic">Toggle <em>G</em>rimoire</li> |         <li @click="togglePublic">Toggle <em>G</em>rimoire</li> | ||||||
|  | @ -74,9 +85,11 @@ import Modal from "./components/Modal"; | ||||||
| import RoleSelectionModal from "./components/RoleSelectionModal"; | import RoleSelectionModal from "./components/RoleSelectionModal"; | ||||||
| import rolesJSON from "./roles"; | import rolesJSON from "./roles"; | ||||||
| import editionJSON from "./editions"; | import editionJSON from "./editions"; | ||||||
|  | import Screenshot from "./components/Screenshot"; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|  |     Screenshot, | ||||||
|     TownSquare, |     TownSquare, | ||||||
|     TownInfo, |     TownInfo, | ||||||
|     Modal, |     Modal, | ||||||
|  | @ -89,6 +102,7 @@ export default { | ||||||
|       isControlOpen: false, |       isControlOpen: false, | ||||||
|       isEditionModalOpen: false, |       isEditionModalOpen: false, | ||||||
|       isRoleModalOpen: false, |       isRoleModalOpen: false, | ||||||
|  |       isScreenshotSuccess: false, | ||||||
|       players: [], |       players: [], | ||||||
|       roles: this.getRolesByEdition(), |       roles: this.getRolesByEdition(), | ||||||
|       edition: "tb", |       edition: "tb", | ||||||
|  | @ -96,6 +110,11 @@ export default { | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     takeScreenshot(dimensions = {}) { | ||||||
|  |       this.isControlOpen = false; | ||||||
|  |       this.isScreenshotSuccess = false; | ||||||
|  |       this.$refs.screenshot.capture(dimensions); | ||||||
|  |     }, | ||||||
|     togglePublic() { |     togglePublic() { | ||||||
|       this.isPublic = !this.isPublic; |       this.isPublic = !this.isPublic; | ||||||
|       this.isControlOpen = false; |       this.isControlOpen = false; | ||||||
|  | @ -266,6 +285,16 @@ ul { | ||||||
|   height: 100%; |   height: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // success animation | ||||||
|  | @keyframes greenToWhite { | ||||||
|  |   from { | ||||||
|  |     color: green; | ||||||
|  |   } | ||||||
|  |   to { | ||||||
|  |     color: white; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Controls | // Controls | ||||||
| .controls { | .controls { | ||||||
|   position: absolute; |   position: absolute; | ||||||
|  | @ -275,6 +304,11 @@ ul { | ||||||
|   padding: 10px; |   padding: 10px; | ||||||
|   svg { |   svg { | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|  |     margin-left: 10px; | ||||||
|  |     &.success { | ||||||
|  |       animation: greenToWhite 1s normal forwards; | ||||||
|  |       animation-iteration-count: 1; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|   ul { |   ul { | ||||||
|     display: flex; |     display: flex; | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| <template> | <template> | ||||||
|   <li> |   <li> | ||||||
|     <div |     <div | ||||||
|  |       ref="player" | ||||||
|       class="player" |       class="player" | ||||||
|       :class="{ |       :class="{ | ||||||
|         dead: player.hasDied, |         dead: player.hasDied, | ||||||
|  | @ -13,7 +14,12 @@ | ||||||
|       <Token :role="player.role" @set-role="setRole" /> |       <Token :role="player.role" @set-role="setRole" /> | ||||||
| 
 | 
 | ||||||
|       <div class="name" @click="changeName"> |       <div class="name" @click="changeName"> | ||||||
|  |         <span class="screenshot" @click.stop="takeScreenshot"> | ||||||
|  |           <font-awesome-icon icon="camera" /> | ||||||
|  |         </span> | ||||||
|  |         <span class="name"> | ||||||
|           {{ player.name }} |           {{ player.name }} | ||||||
|  |         </span> | ||||||
|         <span class="remove" @click.stop="$emit('remove-player', player)"> |         <span class="remove" @click.stop="$emit('remove-player', player)"> | ||||||
|           <font-awesome-icon icon="times-circle" /> |           <font-awesome-icon icon="times-circle" /> | ||||||
|         </span> |         </span> | ||||||
|  | @ -59,6 +65,10 @@ export default { | ||||||
|     return {}; |     return {}; | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     takeScreenshot() { | ||||||
|  |       const { width, height, x, y } = this.$refs.player.getBoundingClientRect(); | ||||||
|  |       this.$emit("screenshot", { width, height, x, y }); | ||||||
|  |     }, | ||||||
|     toggleStatus() { |     toggleStatus() { | ||||||
|       if (this.isPublic) { |       if (this.isPublic) { | ||||||
|         if (!this.player.hasDied) { |         if (!this.player.hasDied) { | ||||||
|  | @ -224,8 +234,11 @@ export default { | ||||||
|   filter: drop-shadow(0 0 1px rgba(0, 0, 0, 1)) |   filter: drop-shadow(0 0 1px rgba(0, 0, 0, 1)) | ||||||
|     drop-shadow(0 0 1px rgba(0, 0, 0, 1)) drop-shadow(0 0 1px rgba(0, 0, 0, 1)); |     drop-shadow(0 0 1px rgba(0, 0, 0, 1)) drop-shadow(0 0 1px rgba(0, 0, 0, 1)); | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|   span { |   white-space: nowrap; | ||||||
|  |   span.screenshot, | ||||||
|  |   span.remove { | ||||||
|     display: none; |     display: none; | ||||||
|  |     margin: 0 10px; | ||||||
|   } |   } | ||||||
|   &:hover { |   &:hover { | ||||||
|     color: red; |     color: red; | ||||||
|  |  | ||||||
							
								
								
									
										83
									
								
								src/components/Screenshot.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/components/Screenshot.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,83 @@ | ||||||
|  | <template> | ||||||
|  |   <div id="screenshot"> | ||||||
|  |     <video ref="video" autoplay></video> | ||||||
|  |     <canvas ref="canvas"></canvas> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   data: function() { | ||||||
|  |     return { | ||||||
|  |       stream: null | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     async capture({ x, y, width, height }) { | ||||||
|  |       const canvas = this.$refs.canvas; | ||||||
|  |       const video = this.$refs.video; | ||||||
|  |       // start capturing | ||||||
|  |       if (!this.stream || !this.stream.active) { | ||||||
|  |         alert( | ||||||
|  |           "Please select to stream the current browser tab to get the appropriate screenshots" | ||||||
|  |         ); | ||||||
|  |         try { | ||||||
|  |           this.stream = await navigator.mediaDevices.getDisplayMedia({ | ||||||
|  |             video: { | ||||||
|  |               // frameRate: 5, | ||||||
|  |               cursor: "never" | ||||||
|  |             }, | ||||||
|  |             audio: false | ||||||
|  |           }); | ||||||
|  |         } catch (err) { | ||||||
|  |           this.$emit("error", err); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       // get screenshot | ||||||
|  |       if (this.stream && this.stream.active) { | ||||||
|  |         video.srcObject = this.stream; | ||||||
|  |         video.play(); | ||||||
|  |         setTimeout(() => { | ||||||
|  |           const context = canvas.getContext("2d"); | ||||||
|  |           canvas.setAttribute("width", width || video.videoWidth); | ||||||
|  |           canvas.setAttribute("height", height || video.videoHeight); | ||||||
|  |           context.drawImage( | ||||||
|  |             video, | ||||||
|  |             x || 0, | ||||||
|  |             y || 0, | ||||||
|  |             width || video.videoWidth, | ||||||
|  |             height || video.videoHeight, | ||||||
|  |             0, | ||||||
|  |             0, | ||||||
|  |             width || video.videoWidth, | ||||||
|  |             height || video.videoHeight | ||||||
|  |           ); | ||||||
|  |           canvas.toBlob(blob => { | ||||||
|  |             try { | ||||||
|  |               // eslint-disable-next-line no-undef | ||||||
|  |               const item = new ClipboardItem({ "image/png": blob }); | ||||||
|  |               navigator.clipboard.write([item]); | ||||||
|  |               this.$emit("success"); | ||||||
|  |             } catch (err) { | ||||||
|  |               this.$emit("error", err); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |         }, 100); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | video { | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  |   display: none; | ||||||
|  | } | ||||||
|  | canvas { | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  |   display: none; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -15,10 +15,12 @@ | ||||||
|         @add-reminder="openReminderModal" |         @add-reminder="openReminderModal" | ||||||
|         @set-role="openRoleModal" |         @set-role="openRoleModal" | ||||||
|         @remove-player="removePlayer" |         @remove-player="removePlayer" | ||||||
|  |         @screenshot="$emit('screenshot', $event)" | ||||||
|       ></Player> |       ></Player> | ||||||
|     </ul> |     </ul> | ||||||
|     <div class="bluffs" v-if="players.length > 6"> |     <div class="bluffs" v-if="players.length > 6" ref="bluffs"> | ||||||
|       <h3>Demon bluffs</h3> |       <h3>Demon bluffs</h3> | ||||||
|  |       <font-awesome-icon icon="camera" @click.stop="takeScreenshot" /> | ||||||
|       <ul> |       <ul> | ||||||
|         <li @click="openRoleModal(bluffs[0])"> |         <li @click="openRoleModal(bluffs[0])"> | ||||||
|           <Token :role="bluffs[0].role"></Token> |           <Token :role="bluffs[0].role"></Token> | ||||||
|  | @ -105,6 +107,10 @@ export default { | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     takeScreenshot() { | ||||||
|  |       const { width, height, x, y } = this.$refs.bluffs.getBoundingClientRect(); | ||||||
|  |       this.$emit("screenshot", { width, height, x, y }); | ||||||
|  |     }, | ||||||
|     openReminderModal(player) { |     openReminderModal(player) { | ||||||
|       this.availableRoles = []; |       this.availableRoles = []; | ||||||
|       this.availableReminders = []; |       this.availableReminders = []; | ||||||
|  | @ -189,8 +195,8 @@ export default { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     > * { |     > * { | ||||||
|       margin-left: -100px; |       margin-left: -78px; | ||||||
|       width: 200px; |       width: 156px; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | @ -256,6 +262,15 @@ export default { | ||||||
|   transform: scale(1); |   transform: scale(1); | ||||||
|   opacity: 1; |   opacity: 1; | ||||||
|   transition: all 200ms ease-in-out; |   transition: all 200ms ease-in-out; | ||||||
|  |   > svg { | ||||||
|  |     position: absolute; | ||||||
|  |     top: 10px; | ||||||
|  |     right: 10px; | ||||||
|  |     cursor: pointer; | ||||||
|  |     &:hover { | ||||||
|  |       color: red; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|   h3 { |   h3 { | ||||||
|     margin-top: 5px; |     margin-top: 5px; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -10,7 +10,8 @@ import { | ||||||
|   faTimesCircle, |   faTimesCircle, | ||||||
|   faCogs, |   faCogs, | ||||||
|   faSearchMinus, |   faSearchMinus, | ||||||
|   faSearchPlus |   faSearchPlus, | ||||||
|  |   faCamera | ||||||
| } from "@fortawesome/free-solid-svg-icons"; | } from "@fortawesome/free-solid-svg-icons"; | ||||||
| import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; | import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; | ||||||
| 
 | 
 | ||||||
|  | @ -23,7 +24,8 @@ library.add( | ||||||
|   faTimesCircle, |   faTimesCircle, | ||||||
|   faCogs, |   faCogs, | ||||||
|   faSearchMinus, |   faSearchMinus, | ||||||
|   faSearchPlus |   faSearchPlus, | ||||||
|  |   faCamera | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| Vue.component("font-awesome-icon", FontAwesomeIcon); | Vue.component("font-awesome-icon", FontAwesomeIcon); | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue