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) | ||||
| 
 | ||||
| **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 | ||||
|  |  | |||
							
								
								
									
										34
									
								
								src/App.vue
									
										
									
									
									
								
							
							
						
						
									
										34
									
								
								src/App.vue
									
										
									
									
									
								
							|  | @ -6,6 +6,7 @@ | |||
|       :players="players" | ||||
|       :roles="roles" | ||||
|       :zoom="zoom" | ||||
|       @screenshot="takeScreenshot" | ||||
|     ></TownSquare> | ||||
| 
 | ||||
|     <Modal | ||||
|  | @ -34,7 +35,17 @@ | |||
|       @close="isRoleModalOpen = false" | ||||
|     ></RoleSelectionModal> | ||||
| 
 | ||||
|     <Screenshot | ||||
|       ref="screenshot" | ||||
|       @success="isScreenshotSuccess = true" | ||||
|     ></Screenshot> | ||||
| 
 | ||||
|     <div class="controls"> | ||||
|       <font-awesome-icon | ||||
|         icon="camera" | ||||
|         @click="takeScreenshot()" | ||||
|         v-bind:class="{ success: isScreenshotSuccess }" | ||||
|       /> | ||||
|       <font-awesome-icon icon="cogs" @click="isControlOpen = !isControlOpen" /> | ||||
|       <ul v-if="isControlOpen"> | ||||
|         <li @click="togglePublic">Toggle <em>G</em>rimoire</li> | ||||
|  | @ -74,9 +85,11 @@ import Modal from "./components/Modal"; | |||
| import RoleSelectionModal from "./components/RoleSelectionModal"; | ||||
| import rolesJSON from "./roles"; | ||||
| import editionJSON from "./editions"; | ||||
| import Screenshot from "./components/Screenshot"; | ||||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     Screenshot, | ||||
|     TownSquare, | ||||
|     TownInfo, | ||||
|     Modal, | ||||
|  | @ -89,6 +102,7 @@ export default { | |||
|       isControlOpen: false, | ||||
|       isEditionModalOpen: false, | ||||
|       isRoleModalOpen: false, | ||||
|       isScreenshotSuccess: false, | ||||
|       players: [], | ||||
|       roles: this.getRolesByEdition(), | ||||
|       edition: "tb", | ||||
|  | @ -96,6 +110,11 @@ export default { | |||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     takeScreenshot(dimensions = {}) { | ||||
|       this.isControlOpen = false; | ||||
|       this.isScreenshotSuccess = false; | ||||
|       this.$refs.screenshot.capture(dimensions); | ||||
|     }, | ||||
|     togglePublic() { | ||||
|       this.isPublic = !this.isPublic; | ||||
|       this.isControlOpen = false; | ||||
|  | @ -266,6 +285,16 @@ ul { | |||
|   height: 100%; | ||||
| } | ||||
| 
 | ||||
| // success animation | ||||
| @keyframes greenToWhite { | ||||
|   from { | ||||
|     color: green; | ||||
|   } | ||||
|   to { | ||||
|     color: white; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Controls | ||||
| .controls { | ||||
|   position: absolute; | ||||
|  | @ -275,6 +304,11 @@ ul { | |||
|   padding: 10px; | ||||
|   svg { | ||||
|     cursor: pointer; | ||||
|     margin-left: 10px; | ||||
|     &.success { | ||||
|       animation: greenToWhite 1s normal forwards; | ||||
|       animation-iteration-count: 1; | ||||
|     } | ||||
|   } | ||||
|   ul { | ||||
|     display: flex; | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| <template> | ||||
|   <li> | ||||
|     <div | ||||
|       ref="player" | ||||
|       class="player" | ||||
|       :class="{ | ||||
|         dead: player.hasDied, | ||||
|  | @ -13,7 +14,12 @@ | |||
|       <Token :role="player.role" @set-role="setRole" /> | ||||
| 
 | ||||
|       <div class="name" @click="changeName"> | ||||
|         <span class="screenshot" @click.stop="takeScreenshot"> | ||||
|           <font-awesome-icon icon="camera" /> | ||||
|         </span> | ||||
|         <span class="name"> | ||||
|           {{ player.name }} | ||||
|         </span> | ||||
|         <span class="remove" @click.stop="$emit('remove-player', player)"> | ||||
|           <font-awesome-icon icon="times-circle" /> | ||||
|         </span> | ||||
|  | @ -59,6 +65,10 @@ export default { | |||
|     return {}; | ||||
|   }, | ||||
|   methods: { | ||||
|     takeScreenshot() { | ||||
|       const { width, height, x, y } = this.$refs.player.getBoundingClientRect(); | ||||
|       this.$emit("screenshot", { width, height, x, y }); | ||||
|     }, | ||||
|     toggleStatus() { | ||||
|       if (this.isPublic) { | ||||
|         if (!this.player.hasDied) { | ||||
|  | @ -224,8 +234,11 @@ export default { | |||
|   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)); | ||||
|   cursor: pointer; | ||||
|   span { | ||||
|   white-space: nowrap; | ||||
|   span.screenshot, | ||||
|   span.remove { | ||||
|     display: none; | ||||
|     margin: 0 10px; | ||||
|   } | ||||
|   &:hover { | ||||
|     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" | ||||
|         @set-role="openRoleModal" | ||||
|         @remove-player="removePlayer" | ||||
|         @screenshot="$emit('screenshot', $event)" | ||||
|       ></Player> | ||||
|     </ul> | ||||
|     <div class="bluffs" v-if="players.length > 6"> | ||||
|     <div class="bluffs" v-if="players.length > 6" ref="bluffs"> | ||||
|       <h3>Demon bluffs</h3> | ||||
|       <font-awesome-icon icon="camera" @click.stop="takeScreenshot" /> | ||||
|       <ul> | ||||
|         <li @click="openRoleModal(bluffs[0])"> | ||||
|           <Token :role="bluffs[0].role"></Token> | ||||
|  | @ -105,6 +107,10 @@ export default { | |||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     takeScreenshot() { | ||||
|       const { width, height, x, y } = this.$refs.bluffs.getBoundingClientRect(); | ||||
|       this.$emit("screenshot", { width, height, x, y }); | ||||
|     }, | ||||
|     openReminderModal(player) { | ||||
|       this.availableRoles = []; | ||||
|       this.availableReminders = []; | ||||
|  | @ -189,8 +195,8 @@ export default { | |||
|     } | ||||
| 
 | ||||
|     > * { | ||||
|       margin-left: -100px; | ||||
|       width: 200px; | ||||
|       margin-left: -78px; | ||||
|       width: 156px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -256,6 +262,15 @@ export default { | |||
|   transform: scale(1); | ||||
|   opacity: 1; | ||||
|   transition: all 200ms ease-in-out; | ||||
|   > svg { | ||||
|     position: absolute; | ||||
|     top: 10px; | ||||
|     right: 10px; | ||||
|     cursor: pointer; | ||||
|     &:hover { | ||||
|       color: red; | ||||
|     } | ||||
|   } | ||||
|   h3 { | ||||
|     margin-top: 5px; | ||||
|   } | ||||
|  |  | |||
|  | @ -10,7 +10,8 @@ import { | |||
|   faTimesCircle, | ||||
|   faCogs, | ||||
|   faSearchMinus, | ||||
|   faSearchPlus | ||||
|   faSearchPlus, | ||||
|   faCamera | ||||
| } from "@fortawesome/free-solid-svg-icons"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; | ||||
| 
 | ||||
|  | @ -23,7 +24,8 @@ library.add( | |||
|   faTimesCircle, | ||||
|   faCogs, | ||||
|   faSearchMinus, | ||||
|   faSearchPlus | ||||
|   faSearchPlus, | ||||
|   faCamera | ||||
| ); | ||||
| 
 | ||||
| Vue.component("font-awesome-icon", FontAwesomeIcon); | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue