34 |
35 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/autocompletado-vue-bootstrap/con-api/script.js:
--------------------------------------------------------------------------------
1 | Vue.component('vue-bootstrap-typeahead', VueBootstrapTypeahead)
2 | new Vue({
3 | el: "#app",
4 | data: () => ({
5 | busqueda: "",
6 | articulos: [],
7 | articuloSeleccionado: {},
8 | }),
9 | methods: {
10 | // Función que se invoca cuando el texto de búsqueda cambia
11 | async buscarArticulos() {
12 | // La búsqueda como string. La tomamos del campo de texto
13 | const busqueda = this.busqueda;
14 | // Hacemos la petición a nuestra API pasándole la búsqueda. En este caso consulto la wikipedia
15 | const respuesta = await fetch(`https://es.wikipedia.org/w/api.php?action=query&list=search&srprop=snippet&format=json&origin=*&utf8=&srsearch=${busqueda}`);
16 | /**
17 | * Pequeña gran nota: En este caso primero decodifico como texto, luego quito el HTML
18 | * y después hago un JSON.parse así:
19 | let datos = await respuesta.text();
20 | datos = this.quitarHTML(datos);
21 | datos = JSON.parse(datos);
22 | * En tu caso, si tu API no devuelve HTML (como debería ser) simplemente haz un:
23 | const datos = await respuesta.json();
24 | */
25 | let articulosWikipedia = await respuesta.text();
26 | articulosWikipedia = this.quitarHTML(articulosWikipedia);
27 | articulosWikipedia = JSON.parse(articulosWikipedia);
28 | // Aquí todo depende de la respuesta de tu servidor. Si el mismo solo te da un arreglo, entonces asigna con:
29 | // this.articulos = datos;
30 | //Pero en este caso la API nos regresa un objeto que tiene la propiedad "query" y dentro de la misma tiene la propiedad search
31 | this.articulos = articulosWikipedia.query.search;
32 | },
33 | quitarHTML(html) {
34 | return html.replace(/<\/?[^>]+>/gi, '');
35 | },
36 | // Función que convierte el objeto a cadena. Es llamado para mostrarse en la lista
37 | serializarValor(articulo) {
38 | return articulo.title + " - " + articulo.snippet;
39 | },
40 | onArticuloSeleccionado(articulo) {
41 | this.articuloSeleccionado = articulo;
42 | }
43 | }
44 | });
--------------------------------------------------------------------------------
/autocompletado-vue-bootstrap/con-api/style.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/parzibyte/ejemplos-javascript/31d317bcce8f9075d2c7a76b8e969a895f800212/autocompletado-vue-bootstrap/con-api/style.css
--------------------------------------------------------------------------------
/autocompletado-vue-bootstrap/estatico/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Autocompletado
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
Autocompletado
22 |
23 |
25 |
26 |
27 | El nombre seleccionado es:
28 | {{nombreSeleccionado}}
29 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/esteganografia/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "autoprefixer": "^10.4.19",
4 | "postcss": "^8.4.38",
5 | "tailwindcss": "^3.4.4"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/esteganografia/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/esteganografia/script.js:
--------------------------------------------------------------------------------
1 | let canvasFueraDePantalla = null;
2 | let contextoCanvas = null;
3 | const CODIGO_CARACTER_TERMINACION = 4;
4 | const $imagen = document.querySelector("#imagen");
5 | const $ocultar = document.querySelector("#ocultar");
6 | const $leer = document.querySelector("#leer");
7 | const $mensaje = document.querySelector("#mensaje");
8 | const $resultado = document.querySelector("#resultado");
9 | let nombreImagenSinExtension = "";
10 | let extensionImagen = "";
11 |
12 | const extraerNombreSinExtension = (nombre) => {
13 | const indice = nombre.lastIndexOf(".");
14 | if (indice === -1) {
15 | return "";
16 | }
17 | return nombre.substring(0, indice);
18 | }
19 | const extraerExtensionDeArchivo = (nombre) => {
20 | const indice = nombre.lastIndexOf(".");
21 | if (indice === -1) {
22 | return "";
23 | }
24 | return nombre.substring(indice + 1);
25 | }
26 | const mimeTypeAPartirDeExtension = (extension) => {
27 | if (extension === "png") {
28 | return "image/png";
29 | } else if (extension === "jpg") {
30 | return "image/jpeg";
31 | } else if (extension === "webp") {
32 | return "image/png";
33 | }
34 | }
35 | /*
36 | Si especificas el indiceBit en 0 te estarás refiriendo al LSB
37 | y si especificas el indiceBit en 7 te refieres al MSB.
38 | Dicho de otra manera, los índices comienzan a contarse de derecha a izquierda
39 | */
40 | const establecerBitEnNumero = (numero, indiceBit, bit) => {
41 | if (bit === 1) {
42 | numero |= (1 << indiceBit);
43 | } else {
44 | numero &= ~(1 << indiceBit);
45 | }
46 | return numero;
47 | }
48 |
49 | // Establece el LSB de un número según el bit y devuelve el número
50 | const colocarLsbDeNumero = (numero, bit) => {
51 | // Solo lo he probado con números entre 0 y 255
52 | if (bit === 1) {
53 | return numero | 1;
54 | } else {
55 | return numero & 254;
56 | }
57 | }
58 |
59 | // Devuelve el LSB de un número
60 | const obtenerLsb = (numero) => {
61 | return numero & 1;
62 | }
63 |
64 | const obtenerBitsDeMensaje = (mensaje) => {
65 | const bits = [];
66 | for (let indiceByte = 0; indiceByte < mensaje.length; indiceByte++) {
67 | // Obtener entero ASCII de la letra (byte) actual...
68 | const charCode = mensaje.charCodeAt(indiceByte);
69 | // Recorrer cada bit
70 | for (let indiceBit = 7; indiceBit >= 0; indiceBit--) {
71 | const bit = (charCode >> indiceBit) & 1;
72 | //console.log(`Charcode ${charCode} con índice bit ${indiceBit} y bit valor ${bit}`);
73 | bits.push(bit);
74 | }
75 | }
76 | return bits;
77 | }
78 |
79 |
80 | const dibujarImagenEnCanvasGlobal = async (imagen) => {
81 | nombreImagenSinExtension = extraerNombreSinExtension(imagen.name);
82 | extensionImagen = extraerExtensionDeArchivo(imagen.name);
83 | const imagenComoBitmap = await createImageBitmap(imagen);
84 | canvasFueraDePantalla = new OffscreenCanvas(imagenComoBitmap.width, imagenComoBitmap.height);
85 | contextoCanvas = canvasFueraDePantalla.getContext("2d");
86 | contextoCanvas.drawImage(imagenComoBitmap, 0, 0, imagenComoBitmap.width, imagenComoBitmap.height);
87 | }
88 |
89 | const obtenerImageData = () => {
90 | return contextoCanvas.getImageData(0, 0, canvasFueraDePantalla.width, canvasFueraDePantalla.height);
91 | }
92 |
93 | const colocarImageData = (datosDeImagen) => {
94 | contextoCanvas.putImageData(datosDeImagen, 0, 0);
95 | }
96 |
97 | const descargarCanvas = async () => {
98 | let fotoComoBlob = await canvasFueraDePantalla.convertToBlob();
99 | const a = document.createElement("a");
100 | const archivo = new Blob([fotoComoBlob], { type: mimeTypeAPartirDeExtension(extensionImagen) });
101 | const url = URL.createObjectURL(archivo);
102 | a.href = url;
103 | a.download = `${nombreImagenSinExtension}_con_mensaje.${extensionImagen}`;
104 | a.click();
105 | URL.revokeObjectURL(url);
106 | }
107 |
108 | const leer = async () => {
109 | const imagenes = $imagen.files;
110 | if (imagenes.length <= 0) {
111 | return;
112 | }
113 | const primerArchivoDeImagen = imagenes[0];
114 | await dibujarImagenEnCanvasGlobal(primerArchivoDeImagen);
115 | const datosDeImagen = obtenerImageData();
116 | const pixeles = datosDeImagen.data;
117 | let mensaje = "";
118 | let codigoDelCaracterActual = 0;
119 | let indiceBitEnCaracterActual = 7;
120 | for (let indice = 0; indice < pixeles.length; indice++) {
121 | // Omitir canal alfa por ahora
122 | // Por ejemplo 3, 7, 11 son el alfa. Si le sumamos 1 son
123 | // 4,8,12 que ya se puede comparar para saber si es múltiplo
124 | // de 4
125 | //console.log({ indice });
126 | if ((indice + 1) % 4 === 0) {
127 | continue;
128 | }
129 | const lsbDelNivelDelColor = obtenerLsb(pixeles[indice]);
130 | codigoDelCaracterActual = establecerBitEnNumero(codigoDelCaracterActual, indiceBitEnCaracterActual, lsbDelNivelDelColor);
131 | //console.log(`Agregando LSB ${lsbDelNivelDelColor} del nivel ${pixeles[indice]} al número que hasta ahora es ${codigoDelCaracterActual} en el índice ${indiceBitEnCaracterActual}`);
132 | if (indiceBitEnCaracterActual === 0) {
133 | if (codigoDelCaracterActual === CODIGO_CARACTER_TERMINACION) {
134 | break;
135 | }
136 | const letra = String.fromCodePoint(codigoDelCaracterActual);
137 | //console.log(`Agregando número ${codigoDelCaracterActual} que es ${letra}`);
138 | mensaje += letra;
139 | codigoDelCaracterActual = 0;
140 | indiceBitEnCaracterActual = 8;
141 | }
142 | indiceBitEnCaracterActual--;
143 | }
144 | $resultado.textContent = mensaje;
145 | }
146 |
147 | const bitsMensajeTerminacion = obtenerBitsDeMensaje(String.fromCharCode(CODIGO_CARACTER_TERMINACION));
148 | const ocultar = async () => {
149 | const imagenes = $imagen.files;
150 | if (imagenes.length <= 0) {
151 | return;
152 | }
153 | const mensaje = $mensaje.value;
154 | if (!mensaje) {
155 | return alert("Escribe un mensaje");
156 | }
157 | const primerArchivoDeImagen = imagenes[0];
158 | await dibujarImagenEnCanvasGlobal(primerArchivoDeImagen);
159 |
160 | const datosDeImagen = obtenerImageData();
161 | const pixeles = datosDeImagen.data;
162 | const bitsMensaje = obtenerBitsDeMensaje(mensaje).concat(bitsMensajeTerminacion);
163 | let indiceDeNivelDeColorDelPixel = 0;
164 | for (let indiceBit = 0; indiceBit < bitsMensaje.length; indiceBit++) {
165 | const bitDelMensaje = bitsMensaje[indiceBit];
166 | //console.log(`Ocultando ${bitDelMensaje} en el nivel con valor ${pixeles[indicePixel]} (posición ${indicePixel}) que será convertido a ${colocarLsbDeNumero(pixeles[indicePixel], bitDelMensaje)}`)
167 | pixeles[indiceDeNivelDeColorDelPixel] = colocarLsbDeNumero(pixeles[indiceDeNivelDeColorDelPixel], bitDelMensaje);
168 | indiceDeNivelDeColorDelPixel++;
169 | // Omitir canal alfa por ahora
170 | // Por ejemplo 3, 7, 11 son el alfa. Si le sumamos 1 son
171 | // 4,8,12 que ya se puede comparar para saber si es múltiplo
172 | // de 4
173 | if ((indiceDeNivelDeColorDelPixel + 1) % 4 === 0) {
174 | indiceDeNivelDeColorDelPixel++;
175 | }
176 | }
177 | colocarImageData(datosDeImagen);
178 | await descargarCanvas();
179 | }
180 |
181 | document.addEventListener("DOMContentLoaded", () => {
182 | $ocultar.addEventListener("click", ocultar);
183 | $leer.addEventListener("click", leer);
184 | });
--------------------------------------------------------------------------------
/esteganografia/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./**/*.html"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/firma-js/documento.html:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Documento con firma - By Parzibyte
42 |
49 |
50 |
51 |
52 |
Título del documento
53 | Simple documento para demostrar cómo se puede colocar una firma del usuario
54 |
Lorem ipsum dolor sit amet consectetur adipisicing elit. Et magnam eius reprehenderit repudiandae, veritatis
55 | aliquid a iste! Eos necessitatibus omnis maiores doloremque? Ipsam rem omnis saepe architecto quam molestias
56 | asperiores.
57 |
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quam unde veritatis, aut exercitationem in voluptatum
58 | aliquid rem deleniti non quas dignissimos asperiores laborum omnis similique esse, neque autem sit possimus.
59 |
Quos veniam incidunt animi distinctio, itaque voluptate laudantium voluptates doloribus ipsa praesentium qui
60 | veritatis perferendis rerum dicta a, non esse cupiditate nemo mollitia exercitationem nesciunt explicabo,
61 | debitis dolores. Mollitia, similique.
62 |
Deleniti sapiente rem beatae officia libero similique iste, vitae aut? Voluptatum aperiam fugit placeat adipisci,
63 | consequatur reiciendis voluptatem eius dolore qui. Cumque delectus iste earum, explicabo error quas rerum nam!
64 |
65 |
Porro tempore ipsa enim a dolore explicabo totam. Quos veniam repellendus quo excepturi voluptatibus eum
66 | provident corrupti debitis nesciunt neque ipsa, consequatur qui illo perferendis mollitia omnis sit cum sunt.
67 |
68 |
Aliquid saepe quod recusandae at adipisci veniam quasi delectus maiores magni fuga accusamus ex, facere, vero
69 | voluptatem temporibus odit maxime. Fuga assumenda suscipit repellat sapiente, porro sit repudiandae doloremque
70 | officiis.
Soy un párrafo solo para agregar contenido y demostrar que el scroll no afecta la firma
47 |
Lorem ipsum dolor sit amet consectetur adipisicing elit. Blanditiis minus voluptatibus facere architecto ipsam
48 | beatae nostrum neque. Architecto voluptatibus amet, sed molestiae, unde pariatur qui mollitia excepturi
49 | provident vel numquam.
50 |
Firmar a continuación:
51 |
52 |
Lorem ipsum dolor sit amet consectetur adipisicing elit. Blanditiis minus voluptatibus facere architecto ipsam
53 | beatae nostrum neque. Architecto voluptatibus amet, sed molestiae, unde pariatur qui mollitia excepturi
54 | provident vel numquam.
55 |
Lorem ipsum dolor sit amet consectetur adipisicing elit. Harum velit atque, dignissimos assumenda architecto
56 | expedita molestias. Velit quis tempore quos consequuntur? Voluptatum labore voluptas sint itaque rerum,
57 | blanditiis officiis a.
58 |
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Maxime quo accusantium doloribus expedita beatae odit
59 | corporis possimus aspernatur explicabo id tempora, voluptatem eligendi ea, libero, asperiores dignissimos
60 | consequatur repellendus dolorum!
61 |
62 |
63 |
64 |
65 |
66 | By Parzibyte
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/firma-js/script.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | ____ _____ _ _ _
4 | | _ \ | __ \ (_) | | |
5 | | |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
6 | | _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
7 | | |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
8 | |____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
9 | __/ | __/ |
10 | |___/ |___/
11 |
12 | ____________________________________
13 | / Si necesitas ayuda, contáctame en \
14 | \ https://parzibyte.me /
15 | ------------------------------------
16 | \ ^__^
17 | \ (oo)\_______
18 | (__)\ )\/\
19 | ||----w |
20 | || ||
21 | Creado por Parzibyte (https://parzibyte.me).
22 | ------------------------------------------------------------------------------------------------
23 | | IMPORTANTE |
24 | Si vas a borrar este encabezado, considera:
25 | Seguirme: https://parzibyte.me/blog/sigueme/
26 | Y compartir mi blog con tus amigos
27 | También tengo canal de YouTube: https://www.youtube.com/channel/UCroP4BTWjfM0CkGB6AFUoBg?sub_confirmation=1
28 | Twitter: https://twitter.com/parzibyte
29 | Facebook: https://facebook.com/parzibyte.fanpage
30 | Instagram: https://instagram.com/parzibyte
31 | Hacer una donación vía PayPal: https://paypal.me/LuisCabreraBenito
32 | ------------------------------------------------------------------------------------------------
33 | */
34 | const $canvas = document.querySelector("#canvas"),
35 | $btnDescargar = document.querySelector("#btnDescargar"),
36 | $btnLimpiar = document.querySelector("#btnLimpiar"),
37 | $btnGenerarDocumento = document.querySelector("#btnGenerarDocumento");
38 | const contexto = $canvas.getContext("2d");
39 | const COLOR_PINCEL = "black";
40 | const COLOR_FONDO = "white";
41 | const GROSOR = 2;
42 | let xAnterior = 0, yAnterior = 0, xActual = 0, yActual = 0;
43 | const obtenerXReal = (clientX) => clientX - $canvas.getBoundingClientRect().left;
44 | const obtenerYReal = (clientY) => clientY - $canvas.getBoundingClientRect().top;
45 | let haComenzadoDibujo = false; // Bandera que indica si el usuario está presionando el botón del mouse sin soltarlo
46 |
47 |
48 | const limpiarCanvas = () => {
49 | // Colocar color blanco en fondo de canvas
50 | contexto.fillStyle = COLOR_FONDO;
51 | contexto.fillRect(0, 0, $canvas.width, $canvas.height);
52 | };
53 | limpiarCanvas();
54 | $btnLimpiar.onclick = limpiarCanvas;
55 | // Escuchar clic del botón para descargar el canvas
56 | $btnDescargar.onclick = () => {
57 | const enlace = document.createElement('a');
58 | // El título
59 | enlace.download = "Firma.png";
60 | // Convertir la imagen a Base64 y ponerlo en el enlace
61 | enlace.href = $canvas.toDataURL();
62 | // Hacer click en él
63 | enlace.click();
64 | };
65 |
66 | window.obtenerImagen = () => {
67 | return $canvas.toDataURL();
68 | };
69 |
70 | $btnGenerarDocumento.onclick = () => {
71 | window.open("documento.html");
72 | };
73 | const onClicOToqueIniciado = evento => {
74 | // En este evento solo se ha iniciado el clic, así que dibujamos un punto
75 | xAnterior = xActual;
76 | yAnterior = yActual;
77 | xActual = obtenerXReal(evento.clientX);
78 | yActual = obtenerYReal(evento.clientY);
79 | contexto.beginPath();
80 | contexto.fillStyle = COLOR_PINCEL;
81 | contexto.fillRect(xActual, yActual, GROSOR, GROSOR);
82 | contexto.closePath();
83 | // Y establecemos la bandera
84 | haComenzadoDibujo = true;
85 | }
86 |
87 | const onMouseODedoMovido = evento => {
88 | evento.preventDefault(); // Prevenir scroll en móviles
89 | if (!haComenzadoDibujo) {
90 | return;
91 | }
92 | // El mouse se está moviendo y el usuario está presionando el botón, así que dibujamos todo
93 | let target = evento;
94 | if (evento.type.includes("touch")) {
95 | target = evento.touches[0];
96 | }
97 | xAnterior = xActual;
98 | yAnterior = yActual;
99 | xActual = obtenerXReal(target.clientX);
100 | yActual = obtenerYReal(target.clientY);
101 | contexto.beginPath();
102 | contexto.moveTo(xAnterior, yAnterior);
103 | contexto.lineTo(xActual, yActual);
104 | contexto.strokeStyle = COLOR_PINCEL;
105 | contexto.lineWidth = GROSOR;
106 | contexto.stroke();
107 | contexto.closePath();
108 | }
109 | const onMouseODedoLevantado = () => {
110 | haComenzadoDibujo = false;
111 | };
112 |
113 | // Lo demás tiene que ver con pintar sobre el canvas en los eventos del mouse
114 | ["mousedown", "touchstart"].forEach(nombreDeEvento => {
115 | $canvas.addEventListener(nombreDeEvento, onClicOToqueIniciado);
116 | });
117 |
118 | ["mousemove", "touchmove"].forEach(nombreDeEvento => {
119 | $canvas.addEventListener(nombreDeEvento, onMouseODedoMovido);
120 | });
121 | ["mouseup", "touchend"].forEach(nombreDeEvento => {
122 | $canvas.addEventListener(nombreDeEvento, onMouseODedoLevantado);
123 | });
--------------------------------------------------------------------------------
/foto-camara-telegram/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
14 | Foto JS + Telegram
15 |
22 |
23 |
24 |
25 |
Tomar foto con JavaScript y enviarla a Telegram
26 |
Selecciona un dispositivo
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/foto-camara-telegram/script.js:
--------------------------------------------------------------------------------
1 | /*
2 | Tomar una fotografía y enviarla a Telegram periódicamente
3 | @date 2024-05-14
4 | @author parzibyte
5 | @web parzibyte.me/blog
6 | */
7 | const tieneSoporteUserMedia = () =>
8 | !!(navigator.getUserMedia || (navigator.mozGetUserMedia || navigator.mediaDevices.getUserMedia) || navigator.webkitGetUserMedia || navigator.msGetUserMedia)
9 | const _getUserMedia = (...arguments) =>
10 | (navigator.getUserMedia || (navigator.mozGetUserMedia || navigator.mediaDevices.getUserMedia) || navigator.webkitGetUserMedia || navigator.msGetUserMedia).apply(navigator, arguments);
11 |
12 | const sleep = ms => new Promise(r => setTimeout(r, ms));
13 | let yaEstaEnviandoFotos = false;
14 | const TIEMPO_ESPERA_ENTRE_FOTOS_EN_MILISEGUNDOS = 2000;
15 | const tomarFotoPeriodicamente = async () => {
16 | while (true) {
17 | await tomarFoto();
18 | await sleep(TIEMPO_ESPERA_ENTRE_FOTOS_EN_MILISEGUNDOS);
19 | }
20 | }
21 | const $video = document.querySelector("#video"),
22 | $estado = document.querySelector("#estado"),
23 | $listaDeDispositivos = document.querySelector("#listaDeDispositivos");
24 | const urlSearchParams = new URLSearchParams(window.location.search);
25 | const TOKEN = urlSearchParams.get("token"),
26 | ID_CHAT = urlSearchParams.get("idChat");
27 | let canvasFueraDePantalla = null;
28 | let contextoCanvas = null;
29 | const formateador = new Intl.DateTimeFormat('es-MX', { dateStyle: 'medium', timeStyle: 'medium' });
30 |
31 | const limpiarSelect = () => {
32 | for (let x = $listaDeDispositivos.options.length - 1; x >= 0; x--)
33 | $listaDeDispositivos.remove(x);
34 | };
35 | const obtenerDispositivos = () => navigator
36 | .mediaDevices
37 | .enumerateDevices();
38 |
39 | // La función que es llamada después de que ya se dieron los permisos
40 | // Lo que hace es llenar el select con los dispositivos obtenidos
41 | const llenarSelectConDispositivosDisponibles = async () => {
42 | limpiarSelect();
43 | const dispositivos = await obtenerDispositivos();
44 | const dispositivosDeVideo = [];
45 | dispositivos.forEach(dispositivo => {
46 | const tipo = dispositivo.kind;
47 | if (tipo === "videoinput") {
48 | dispositivosDeVideo.push(dispositivo);
49 | }
50 | });
51 |
52 | // Vemos si encontramos algún dispositivo, y en caso de que si, entonces llamamos a la función
53 | if (dispositivosDeVideo.length > 0) {
54 | // Llenar el select
55 | dispositivosDeVideo.forEach(dispositivo => {
56 | const option = document.createElement('option');
57 | option.value = dispositivo.deviceId;
58 | option.text = dispositivo.label;
59 | $listaDeDispositivos.appendChild(option);
60 | });
61 | }
62 | }
63 |
64 | const enviarImagen = async (idChat, token, imagen) => {
65 | const url = `https://api.telegram.org/bot${token}/sendPhoto`;
66 | const fd = new FormData();
67 | fd.append("chat_id", idChat)
68 | const caption = `${$listaDeDispositivos.options[$listaDeDispositivos.selectedIndex].text} ${formateador.format(new Date())}`;
69 | fd.append("caption", caption);
70 | fd.append("photo", imagen);
71 | const respuestaHttp = await fetch(url, {
72 | method: 'POST',
73 | body: fd,
74 | });
75 | return {
76 | respuesta: await respuestaHttp.json(),
77 | codigo: respuestaHttp.status,
78 | };
79 | }
80 |
81 | const tomarFoto = async () => {
82 | //Pausar reproducción
83 | $video.pause();
84 | canvasFueraDePantalla.width = $video.videoWidth;
85 | canvasFueraDePantalla.height = $video.videoHeight;
86 | //Obtener contexto del canvas y dibujar sobre él
87 | contextoCanvas.drawImage($video, 0, 0, canvasFueraDePantalla.width, canvasFueraDePantalla.height);
88 | //Reanudar reproducción
89 | await $video.play();
90 | let foto = await canvasFueraDePantalla.convertToBlob(); //Esta es la foto como bytes
91 | await enviarImagen(ID_CHAT, TOKEN, foto);
92 | }
93 |
94 | (async function () {
95 | // Comenzamos viendo si tiene soporte, si no, nos detenemos
96 | if (!tieneSoporteUserMedia()) {
97 | alert("Lo siento. Tu navegador no soporta esta característica");
98 | $estado.innerHTML = "Parece que tu navegador no soporta esta característica. Intenta actualizarlo.";
99 | return;
100 | }
101 | //Aquí guardaremos el stream globalmente
102 | let stream;
103 |
104 | const mostrarStream = idDeDispositivo => {
105 | _getUserMedia({
106 | video: {
107 | // Justo aquí indicamos cuál dispositivo usar
108 | deviceId: idDeDispositivo,
109 | }
110 | },
111 | async (streamObtenido) => {
112 | // Aquí ya tenemos permisos, ahora sí llenamos el select,
113 | // pues si no, no nos daría el nombre de los dispositivos
114 | if ($listaDeDispositivos.length <= 0) {
115 | llenarSelectConDispositivosDisponibles();
116 | }
117 |
118 | // Escuchar cuando seleccionen otra opción y entonces llamar a esta función
119 | $listaDeDispositivos.onchange = () => {
120 | // Detener el stream
121 | if (stream) {
122 | stream.getTracks().forEach(function (track) {
123 | track.stop();
124 | });
125 | }
126 | // Mostrar el nuevo stream con el dispositivo seleccionado
127 | mostrarStream($listaDeDispositivos.value);
128 | }
129 |
130 | // Simple asignación
131 | stream = streamObtenido;
132 |
133 | // Mandamos el stream de la cámara al elemento de vídeo
134 | $video.srcObject = stream;
135 | // Refrescar el canvas
136 | await $video.play();
137 | canvasFueraDePantalla = new OffscreenCanvas($video.videoWidth, $video.videoHeight);
138 | contextoCanvas = canvasFueraDePantalla.getContext("2d");
139 | // Prevenir enviar dos fotos cuando se cambia el dispositivo
140 | if (!yaEstaEnviandoFotos) {
141 | tomarFotoPeriodicamente();
142 | yaEstaEnviandoFotos = true;
143 | }
144 | }, (error) => {
145 | console.log("Permiso denegado o error: ", error);
146 | $estado.innerHTML = "No se puede acceder a la cámara, o no diste permiso.";
147 | });
148 | }
149 |
150 | // Comenzamos pidiendo los dispositivos
151 | const dispositivos = await obtenerDispositivos()
152 | // Vamos a filtrarlos y guardar aquí los de vídeo
153 | const dispositivosDeVideo = [];
154 |
155 | // Recorrer y filtrar
156 | dispositivos.forEach(function (dispositivo) {
157 | const tipo = dispositivo.kind;
158 | if (tipo === "videoinput") {
159 | dispositivosDeVideo.push(dispositivo);
160 | }
161 | });
162 |
163 | // Vemos si encontramos algún dispositivo, y en caso de que si, entonces llamamos a la función
164 | // y le pasamos el id de dispositivo
165 | if (dispositivosDeVideo.length > 0) {
166 | // Mostrar stream con el ID del primer dispositivo, luego el usuario puede cambiar
167 | mostrarStream(dispositivosDeVideo[0].deviceId);
168 | }
169 | })();
--------------------------------------------------------------------------------
/generar-numeros-aleatorios/estilos.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Arial, sans-serif;
3 | background-color: #f5f5f5;
4 | color: #333;
5 | margin: 0;
6 | padding: 20px;
7 | }
8 |
9 | h1 {
10 | color: #4A90E2;
11 | text-align: center;
12 | }
13 |
14 | .form-group {
15 | margin-bottom: 15px;
16 | }
17 |
18 | label {
19 | display: block;
20 | margin-bottom: 5px;
21 | color: #666;
22 | }
23 |
24 | input[type="number"],
25 | textarea {
26 | width: 100%;
27 | padding: 10px;
28 | border: 1px solid #ddd;
29 | border-radius: 5px;
30 | font-size: 16px;
31 | color: #333;
32 | box-sizing: border-box;
33 | }
34 |
35 | input[type="number"]:focus,
36 | textarea:focus {
37 | border-color: #4A90E2;
38 | outline: none;
39 | }
40 |
41 | textarea {
42 | height: 80px;
43 | resize: none;
44 | }
45 |
46 | button {
47 | background-color: #4A90E2;
48 | color: #fff;
49 | border: none;
50 | border-radius: 5px;
51 | padding: 10px 20px;
52 | font-size: 16px;
53 | cursor: pointer;
54 | transition: background-color 0.3s ease;
55 | margin-right: 10px;
56 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
57 | }
58 |
59 | button:hover {
60 | background-color: #357ABD;
61 | }
62 |
63 | button:focus {
64 | outline: none;
65 | box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.5);
66 | }
67 |
68 | button:active {
69 | background-color: #2E5A8C;
70 | }
71 |
72 | pre#contenedor {
73 | background-color: #f9f9f9;
74 | border: 1px solid #ddd;
75 | border-radius: 5px;
76 | padding: 15px;
77 | font-size: 16px;
78 | color: #333;
79 | overflow-x: auto;
80 | max-height: 200px;
81 | margin-top: 20px;
82 | }
--------------------------------------------------------------------------------
/generar-numeros-aleatorios/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Generar números aleatorios
8 |
9 |
10 |
11 |
12 |