mirror of https://github.com/bra1n/townsquare.git
add screenshot functionality
This commit is contained in:
parent
429d9845b1
commit
be44e977eb
|
@ -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">
|
||||||
{{ 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;
|
||||||
|
|
|
@ -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…
Reference in New Issue