add screenshot functionality

This commit is contained in:
Steffen 2020-04-19 22:54:25 +02:00
parent 429d9845b1
commit be44e977eb
No known key found for this signature in database
GPG Key ID: 764D74E98267DFC6
6 changed files with 154 additions and 15 deletions

View File

@ -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

View File

@ -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;

View File

@ -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">
{{ player.name }} <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)"> <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;

View 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>

View File

@ -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;
} }

View File

@ -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);