🎉
This commit is contained in:
136
src/modules/controls/ControlApp.vue
Normal file
136
src/modules/controls/ControlApp.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div style="background-color: white; display: flex; flex-direction: row;gap: 0.5rem;padding: 0.5rem">
|
||||
<div style="max-width: 32px; height: 32px; overflow: hidden;">
|
||||
<google-cast-launcher style="--disconnected-color: black;" ></google-cast-launcher>
|
||||
</div>
|
||||
<button @click="openMapWindow">Open map window</button>
|
||||
<button @click="onStreamImage">Stream image</button>
|
||||
</div>
|
||||
<fieldset>
|
||||
<legend>Image settings</legend>
|
||||
<label>Width</label>
|
||||
<input type="number" v-model="imageWidth" />
|
||||
<label>Screen Diameter</label>
|
||||
<input type="number" v-model="screenDiameterInch" />
|
||||
<div>DPI: {{ screenDPI }}</div>
|
||||
<label>Flip <input type="checkbox" v-model="flipped"></label>
|
||||
<label>Position (X/Y)</label>
|
||||
<input type="range" min="0" max="100" v-model="positionX" />
|
||||
<input type="range" min="0" max="100" v-model="positionY" />
|
||||
<button @click="positionX = 50;positionY = 50">center</button>
|
||||
</fieldset>
|
||||
<FileManager @openFile="openFile" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import FileManager from '../../components/FileManager/FileManager.vue';
|
||||
import type { FileWithMeta } from '../../model/FileManager';
|
||||
import {ref, inject, watch} from 'vue';
|
||||
import {SenderSymbol} from '../../model/injectionSymbols';
|
||||
|
||||
const openedFile = ref<FileWithMeta>()
|
||||
const sender = inject(SenderSymbol);
|
||||
|
||||
const imageWidth = ref(20);
|
||||
const screenDiameterInch = ref(65);
|
||||
const flipped = ref(false);
|
||||
const positionX = ref(50);
|
||||
const positionY = ref(50);
|
||||
|
||||
|
||||
watch(() => imageWidth.value, (value) => {
|
||||
sender?.sendMessage({
|
||||
type: 'changeWidth',
|
||||
width: value
|
||||
})
|
||||
});
|
||||
|
||||
watch(() => screenDiameterInch.value, (value) => {
|
||||
sender?.sendMessage({
|
||||
type: 'changeDiameter',
|
||||
diameter: value
|
||||
})
|
||||
});
|
||||
|
||||
watch(() => [positionX.value, positionY.value, flipped.value], (value) => {
|
||||
sender?.sendMessage({
|
||||
type: 'changePosition',
|
||||
x: positionX.value,
|
||||
y: positionY.value,
|
||||
flipped: flipped.value,
|
||||
})
|
||||
});
|
||||
|
||||
function openFile(file: FileWithMeta) {
|
||||
sender?.sendMessage({
|
||||
type: 'showFile',
|
||||
file,
|
||||
})
|
||||
imageWidth.value = file.width ?? 20;
|
||||
openedFile.value = file;
|
||||
}
|
||||
|
||||
function openMapWindow() {
|
||||
window.open('/map/', 'map');
|
||||
}
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
function onStreamImage() {
|
||||
const image = new Image()
|
||||
image.onload = async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.setAttribute('willReadFrequently', 'true');
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const [maxWidth, maxHeight] = (image.width > image.height) ? [3840, 2160] : [2160, 3840];
|
||||
if (image.width > maxWidth || image.height > maxHeight) {
|
||||
const ratio = Math.min(maxWidth / image.width, maxHeight / image.height);
|
||||
canvas.width = image.width * ratio;
|
||||
canvas.height = image.height * ratio;
|
||||
}
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
const chunkSize = {width: 128, height: 128};
|
||||
|
||||
for (let x = 0; x < canvas.width; x += chunkSize.width) {
|
||||
for (let y = 0; y < canvas.height; y += chunkSize.height) {
|
||||
const imageData = ctx.getImageData(x, y, chunkSize.width, chunkSize.height);
|
||||
sender?.sendMessage({
|
||||
type: 'streamImage',
|
||||
imageData: {
|
||||
data: [...new Uint32Array(imageData.data.buffer)],
|
||||
width: imageData.width,
|
||||
height: imageData.height,
|
||||
},
|
||||
canvas: {
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
},
|
||||
x,
|
||||
y,
|
||||
}, {webRTC: true});
|
||||
await delay(100);
|
||||
}
|
||||
}
|
||||
};
|
||||
image.src = openedFile.value?.path;
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.aside {
|
||||
position: fixed;
|
||||
overflow: auto;
|
||||
width: 300px;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
right:0;
|
||||
background: rgba(0,0,0,0.8);
|
||||
border-left: #555 2px solid;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
</style>
|
||||
132
src/modules/controls/OldApp.vue
Normal file
132
src/modules/controls/OldApp.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend>Image settings</legend>
|
||||
<label>Width</label>
|
||||
<input type="number" v-model="imageWidth" />
|
||||
<label>Screen Diameter</label>
|
||||
<input type="number" v-model="screenDiameterInch" />
|
||||
<div>DPI: {{ screenDPI }}</div>
|
||||
<div style="max-width: 32px; height: 32px; overflow: hidden;">
|
||||
<google-cast-launcher style="max-width: 2rem" ></google-cast-launcher>
|
||||
</div>
|
||||
</fieldset>
|
||||
<FileManager @openFile="openFile" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import FileManager from '../../components/FileManager/FileManager.vue';
|
||||
import type { FileWithMeta } from '../../model/FileManager';
|
||||
import {ref, computed, inject, watch} from 'vue';
|
||||
import {CastTypeSymbol, CastReceiverSymbol, CastSenderSymbol} from '../../model/injectionSymbols';
|
||||
|
||||
const openedFile = ref<FileWithMeta>()
|
||||
const castType = inject(CastTypeSymbol);
|
||||
const receiver = inject(CastReceiverSymbol);
|
||||
const sender = inject(CastSenderSymbol);
|
||||
const canvas = ref<HTMLCanvasElement>();
|
||||
|
||||
const imageWidth = ref(20);
|
||||
const screenDiameterInch = ref(65);
|
||||
|
||||
const screenDPI = computed(() => {
|
||||
const diaPx = Math.sqrt(window.screen.width**2 + window.screen.height ** 2);
|
||||
return diaPx / screenDiameterInch.value;
|
||||
})
|
||||
|
||||
watch(() => imageWidth.value, (value) => {
|
||||
if (sender) {
|
||||
sender.sendMessage({
|
||||
type: 'imageWidth',
|
||||
imageWidth: value
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
function openFile(file: FileWithMeta) {
|
||||
console.log('open file', file)
|
||||
openedFile.value = file
|
||||
imageWidth.value = file.width ?? 20;
|
||||
if (sender) {
|
||||
sender.sendMessage({
|
||||
type: 'loadMap',
|
||||
file
|
||||
})
|
||||
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
canvas.value!.width = img.naturalWidth;
|
||||
canvas.value!.height = img.naturalHeight;
|
||||
const ctx = canvas.value!.getContext('2d')!;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const msgs = [];
|
||||
const chunkWidth = 32;
|
||||
const chunkHeight = 32;
|
||||
|
||||
for (let x = 0; x< 1920; x += chunkWidth) {
|
||||
for (let y = 0; y < 1080; y += chunkHeight) {
|
||||
const data = ctx.getImageData(x, y, chunkWidth, chunkHeight);
|
||||
const msg = {
|
||||
type: 'imageData',
|
||||
imageData: {
|
||||
data: [...data.data],
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
x,
|
||||
y,
|
||||
},
|
||||
};
|
||||
msgs.push(msg);
|
||||
sender.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
ctx.clearRect(0, 0, img.naturalWidth, img.naturalHeight);
|
||||
for (const msg of msgs) {
|
||||
loadImageData(msg);
|
||||
}
|
||||
};
|
||||
img.src = file.path;
|
||||
}
|
||||
}
|
||||
|
||||
function loadImageData(detail) {
|
||||
const imageData = new ImageData(
|
||||
new Uint8ClampedArray(detail.imageData.data),
|
||||
detail.imageData.width,
|
||||
detail.imageData.height
|
||||
)
|
||||
const ctx = canvas.value!.getContext('2d')!;
|
||||
ctx.putImageData(imageData, detail.imageData.x, detail.imageData.y);
|
||||
}
|
||||
|
||||
if (receiver) {
|
||||
receiver.addEventListener('message', ({detail}) => {
|
||||
if (detail.type === 'loadMap') {
|
||||
openFile(detail.file);
|
||||
}
|
||||
if (detail.type === 'imageWidth') {
|
||||
imageWidth.value = detail.imageWidth;
|
||||
}
|
||||
if (detail.type === 'imageData') {
|
||||
loadImageData(detail);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.aside {
|
||||
position: fixed;
|
||||
overflow: auto;
|
||||
width: 300px;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
right:0;
|
||||
background: rgba(0,0,0,0.8);
|
||||
border-left: #555 2px solid;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
</style>
|
||||
64
src/modules/map/MapApp.vue
Normal file
64
src/modules/map/MapApp.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="map-app">
|
||||
<MapScene
|
||||
ref="map"
|
||||
:src="src"
|
||||
:dpi="screenDPI"
|
||||
:flipped="flipped"
|
||||
:width="imageWidth"
|
||||
:position="{x: positionX, y: positionY}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import MapScene from './MapScene.vue';
|
||||
import {inject, ref, computed} from 'vue';
|
||||
import { ReceiverSymbol } from '../../model/injectionSymbols';
|
||||
|
||||
const imageWidth = ref(20);
|
||||
const screenDiameterInch = ref(65);
|
||||
const positionX = ref(50);
|
||||
const positionY = ref(50);
|
||||
const flipped = ref(false);
|
||||
const src = ref<string>()
|
||||
const map = ref<typeof MapScene>();
|
||||
|
||||
const screenDPI = computed(() => {
|
||||
const diaPx = Math.sqrt(window.screen.width**2 + window.screen.height ** 2);
|
||||
return diaPx / screenDiameterInch.value;
|
||||
})
|
||||
|
||||
|
||||
const receiver = inject(ReceiverSymbol);
|
||||
|
||||
receiver?.addEventListener('changeDiameter', ({detail}) => {
|
||||
screenDiameterInch.value = detail.diameter;
|
||||
});
|
||||
|
||||
receiver?.addEventListener('changeWidth', ({detail}) => {
|
||||
imageWidth.value = detail.width;
|
||||
});
|
||||
|
||||
receiver?.addEventListener('showFile', ({detail}) => {
|
||||
src.value = detail.file.path;
|
||||
if (detail.file.width) {
|
||||
imageWidth.value = detail.file.width;
|
||||
}
|
||||
});
|
||||
|
||||
receiver?.addEventListener('changePosition', ({detail}) => {
|
||||
positionX.value = detail.x;
|
||||
positionY.value = detail.y;
|
||||
flipped.value = detail.flipped;
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.map-app {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
}
|
||||
</style>
|
||||
148
src/modules/map/MapScene.vue
Normal file
148
src/modules/map/MapScene.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<video
|
||||
v-if="type === 'video'"
|
||||
:src="src"
|
||||
:style="dynamicStyle"
|
||||
class="map-scene--video map-scene"
|
||||
ref="video"
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
></video>
|
||||
<canvas
|
||||
v-else
|
||||
:style="dynamicStyle"
|
||||
ref="canvas"
|
||||
class="map-scene--image map-scene"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, defineProps, inject, ref, watch } from 'vue';
|
||||
import {ReceiverSymbol} from '../../model/injectionSymbols';
|
||||
|
||||
const video = ref<HTMLVideoElement>();
|
||||
const image = ref<HTMLImageElement>();
|
||||
const canvas = ref<HTMLCanvasElement>();
|
||||
|
||||
const props = defineProps<{
|
||||
src?: string;
|
||||
width: number;
|
||||
dpi: number;
|
||||
position: {x: number, y: number};
|
||||
flipped: boolean;
|
||||
}>();
|
||||
|
||||
const isImage = computed(() => props.src?.endsWith('.png') || props.src?.endsWith('.jpg') || props.src?.endsWith('.jpeg') || props.src?.endsWith('.webp'));
|
||||
const isVideo = computed(() => props.src?.endsWith('.mp4') || props.src?.endsWith('.webm'));
|
||||
|
||||
const mediaDimensions = ref({width: 0, height: 0});
|
||||
const virtualMediaDimensions = ref({width: 0, height: 0});
|
||||
|
||||
const windowSize = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
};
|
||||
|
||||
watch(() => props.src, (src) => {
|
||||
if (isImage.value) {
|
||||
const img = new Image();
|
||||
img.src = src!;
|
||||
img.onload = () => {
|
||||
mediaDimensions.value = {
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight,
|
||||
};
|
||||
canvas.value!.width = img.naturalWidth;
|
||||
canvas.value!.height = img.naturalHeight;
|
||||
const ctx = canvas.value!.getContext('2d')!;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
};
|
||||
} else if (isVideo.value) {
|
||||
video.value!.src = src!;
|
||||
}
|
||||
});
|
||||
|
||||
const receiver = inject(ReceiverSymbol);
|
||||
receiver?.addEventListener('streamImage', ({detail}) => {
|
||||
if (canvas.value) {
|
||||
if (canvas.value.width !== detail.canvas.width || canvas.value.height !== detail.canvas.height) {
|
||||
const backup = document.createElement('canvas');
|
||||
backup.width = canvas.value.width;
|
||||
backup.height = canvas.value.height;
|
||||
const backupCtx = backup.getContext('2d')!;
|
||||
backupCtx.drawImage(canvas.value, 0, 0);
|
||||
canvas.value.width = detail.canvas.width;
|
||||
canvas.value.height = detail.canvas.height;
|
||||
const ctx = canvas.value.getContext('2d')!;
|
||||
ctx.drawImage(backup, 0, 0, detail.canvas.width, detail.canvas.height);
|
||||
}
|
||||
|
||||
const imageData = new ImageData(
|
||||
new Uint8ClampedArray(new Uint32Array(detail.imageData.data).buffer),
|
||||
detail.imageData.width,
|
||||
detail.imageData.height
|
||||
);
|
||||
const ctx = canvas.value.getContext('2d')!;
|
||||
ctx.putImageData(imageData, detail.x, detail.y);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const type = computed(() => {
|
||||
if (isImage.value) {
|
||||
return 'image';
|
||||
} else if (isVideo.value) {
|
||||
return 'video';
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
});
|
||||
|
||||
const dynamicStyle = computed(() => {
|
||||
const width = props.dpi * props.width;
|
||||
return {
|
||||
'--width': `${width}px`,
|
||||
'--height': `${(width) * (mediaDimensions.value.height / mediaDimensions.value.width)}px`,
|
||||
'--x': `${props.position.x/100}`,
|
||||
'--y': `${props.position.y/100}`,
|
||||
'--rotate': mediaDimensions.value.width > mediaDimensions.value.height ? (props.flipped ? '180deg' : '0deg') :
|
||||
(props.flipped ? '90deg' : '-90deg'),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
function onImageLoaded(event: Event) {
|
||||
console.log('image loaded', event.target);
|
||||
mediaDimensions.value = {
|
||||
width: image.value!.naturalWidth,
|
||||
height: image.value!.naturalHeight,
|
||||
};
|
||||
}
|
||||
|
||||
function onVideoLoaded(event: Event) {
|
||||
console.log('video loaded', event.target);
|
||||
mediaDimensions.value = {
|
||||
width: video.value!.videoWidth,
|
||||
height: video.value!.videoHeight,
|
||||
};
|
||||
}
|
||||
|
||||
function updateImageSize() {
|
||||
if (image.value) {
|
||||
image.value.width = props.width;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.map-scene {
|
||||
position: absolute;
|
||||
left: calc(var(--x) * 100vw - var(--width) / 2);
|
||||
top: calc(var(--y) * 100vh - var(--height) / 2);
|
||||
width: var(--width);
|
||||
transform: rotate(var(--rotate));
|
||||
transform-origin: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user