Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                #juego
	#visor
		h1 Sopa de Letras
		p Encuentra estas palabras:
		ol#palabras
		#puntaje
			span Puntaje:&nbsp;
			span.valor 0

		#botones
			button#reiniciar Reiniciar

	#lienzo
		#cuadricula

              
            
!

CSS

              
                @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700;800&display=swap')

:root
	--celda: 40px
	--espacio: calc(var(--celda) * 0.1)

html, body
	background-color: black
	height: 100vh
	width: 100vw
	display: grid
	place-items: center
	font-family: 'Open Sans', sans-serif
	overflow: hidden

	*
		// outline: 1px solid fuchsia

#juego
	aspect-ratio: 16 / 9
	// height: 100%
	// max-height: 100min
	// max-width: 100vw
	// width: 50%
	margin: 0 auto
	display: grid
	grid-template-columns: auto auto
	grid-template-rows: auto
	background-color: whitesmoke
	align-items: center
	transition: transform 50ms linear

#lienzo
	aspect-ratio: 1
	padding: 2rem
	display: grid
	place-items: center
	place-content: center

#cuadricula
	position: relative
	// max-height: 100%
	// width: 100%
	// aspect-ratio: 1
	display: grid
	place-items: center
	place-content: center
	user-select: none
	// gap: calc(var(--celda) * 0.2)
	gap: var(--espacio)

	// gap: 0.2rem
	.letra
		aspect-ratio: 1
		// padding: 0.2rem
		// background-color: pink
		// background-color: alpha(gainsboro, 0.75)
		// border: 1px solid grey
		// margin: -1px
		display: grid
		place-items: center
		place-content: center
		// min-width: 2.5em
		width: var(--celda)
		// background-color: alpha(gainsboro, 0.75)
		border-radius: 100%
		cursor: pointer
		text-align: center
		font-size: 1.2rem
		font-weight: bold

		&:hover, &:active, &.activo
			// background-color: alpha(gold, 0.75)

		span
			display: block
			background-color: transparent
			aspect-ratio: 1
			width: 1.5em
			border-radius: 100%
			text-align: center
			// display: block
			// aspect-ratio: 1
			// width: 40px
			// border-radius: 100%
			// background-color: green

		&.resaltada
			outline: calc(var(--espacio) * 0.5) solid silver
			background-color: dimgray

		&.ignorada
			opacity: 0.5
			color: grey

#visor
	background-color: dimgray
	padding: 1rem 2rem
	color: whitesmoke
	align-self: stretch

.linea
	position: absolute
	pointer-events: none
	background-color: alpha(gold, 0.5)
	// mix-blend-mode: color
	// position: relative
	// opacity: 0
	// grid-column: 1
	// grid-row: 1
	// border-right: calc((var(--celda) / 2)) solid orange
	z-index: 1
	top: 0
	left: 0
	width: var(--celda)
	height: var(--celda)
	border-radius: var(--celda)
	// transform: rotate(45deg)
	transform-origin: calc((var(--celda) / 2))
	transition: background 0.3s, border 0.2s

	&.correcta
		background-color: transparent
		border: 3px solid green

	&.incorrecta
		background-color: alpha(red, 0.5)
		border: 3px solid darkred

#palabras
	color: whitesmoke
	font-weight: bold
	columns: 2
	column-rule: 1px solid grey
	list-style: decimal-leading-zero
	list-style-position: inside
	padding: 0

	li
		white-space: nowrap

	// column-gap: 0.5rem
	.encontrada
		text-decoration: line-through
		color: limegreen

#puntaje
	color: whitesmoke

	.valor
		font-weight: bold

#botones
	display: flex

	button
		display: block
		background-color: grey
		border: none
              
            
!

JS

              
                Sugar.extend();

// clase para manejar el area
class Rectangulo {
	constructor(x = 0, y = 0, horizontal = 1, vertical = 1) {
		this.x = parseInt(x);
		this.y = parseInt(y);
		this.horizontal = parseInt(horizontal);
		this.vertical = parseInt(vertical);

		this.ancho = Math.abs(this.horizontal - 1);
		this.alto = Math.abs(this.vertical - 1);
		this.dHorizontal = Math.sign(this.horizontal);
		this.dVertical = Math.sign(this.vertical);
	}

	// esquina superior izquierda
	get inicio() {
		return {
			x: Math.min(this.x, this.x + this.ancho * this.dHorizontal),
			y: Math.min(this.y, this.y + this.alto * this.dVertical)
		};
	}

	// Esquina inferior derecha
	get fin() {
		return {
			x: Math.max(this.x, this.x + this.ancho * this.dHorizontal),
			y: Math.max(this.y, this.y + this.alto * this.dVertical)
		};
	}

	estaDentro(punto) {
		return (
			this.inicio.x <= punto.x &&
			punto.x <= this.fin.x &&
			this.inicio.y <= punto.y &&
			punto.y <= this.fin.y
		);
	}
}
// fin clase Rectangulo

// clase para manejar las posiciones
class Punto {
	static DIRECCIONES = ["N", "S", "O", "E", "NE", "NO", "SE", "SO"];
	static ANGULOS = {
		"-90": "N",
		"-45": "NE",
		0: "E",
		45: "SE",
		90: "S",
		"-225": "SO",
		"-180": "O",
		"-135": "NO"
	};
	static SALTOS = {
		N: { x: 0, y: -1 },
		S: { x: 0, y: 1 },
		O: { x: -1, y: 0 },
		E: { x: 1, y: 0 },
		NE: { x: 1, y: -1 },
		NO: { x: -1, y: -1 },
		SE: { x: 1, y: 1 },
		SO: { x: -1, y: 1 }
	};

	constructor(x = 0, y = 0) {
		this.x = parseInt(x);
		this.y = parseInt(y);
	}

	desplazar(delta = { x: 1, y: 1 }) {
		this.x += parseInt(delta.x);
		this.y += parseInt(delta.y);
	}

	mover(direccion, distancia = 1) {
		const salto = Punto.SALTOS[direccion];
		this.x += parseInt(salto.x * distancia);
		this.y += parseInt(salto.y * distancia);
	}

	saltar(direccion, distancia) {
		const p = new Punto(this.x, this.y);
		const salto = { ...Punto.SALTOS[direccion] };

		salto.x *= distancia;
		salto.y *= distancia;
		p.desplazar(salto);

		return p;
	}

	*recorrido(fin) {
		const rumbo = {
			x: Math.sign(fin.x - this.x),
			y: Math.sign(fin.y - this.y)
		};

		const punto = new Punto(this.x, this.y);
		yield punto;
		while (!punto.esIgual(fin)) {
			punto.desplazar(rumbo);
			yield punto;
		}
	}

	esIgual(punto) {
		return this.x == punto.x && this.y == punto.y;
	}

	angulo(punto) {
		return Math.atan2(punto.y - this.y, punto.x - this.x) * (180 / Math.PI);
	}

	distancia(punto) {
		return _.round(
			Math.sqrt(Math.pow(punto.y - this.y, 2) + Math.pow(punto.x - this.x, 2)),
			3
		);
	}
}
// fin clase Punto

// Clase para manejar la logica del pupiletras
class Sopa {
	static LETRAS = "ABCDEFGHIJKLMNÑOPQRSTUVWXYZ";

	constructor(ancho, alto, lista) {
		this.area = new Rectangulo(0, 0, ancho, alto);
		this.lista = lista;
		this.palabras = new Map();

		this.matriz = new Array();
		for (let f = 0; f < alto; f++) {
			const fila = new Array();
			for (let c = 0; c < ancho; c++) {
				fila.push({
					letra: "",
					palabras: new Array()
				});
			}
			this.matriz.push(fila);
		}

		// ordeno las palabras desde la mas larga a la mas corta
		this.lista.sort((a, b) => {
			if (a.length === b.length) return b.index - a.index;
			return b.length - a.length;
		});

		const buclesMaximos = this.area.ancho * this.area.alto * 3;

		const excedentes = {
			largo: new Array(),
			espacio: new Array()
		};

		this.lista.forEach((palabra) => {
			if (palabra.length > this.area.ancho && palabra.length > this.area.alto) {
				excedentes.largo.push(palabra);
				return false;
			}

			const letras = this.normalizar(palabra);

			let cabe = false;
			let punto = new Punto();
			let direccion = "S";
			let umbral = buclesMaximos;

			do {
				punto.x = _.random(0, this.area.ancho);
				punto.y = _.random(0, this.area.alto);
				direccion = _.sample(Punto.DIRECCIONES);
				cabe = this.probar(letras, punto, direccion);
				umbral--;
			} while (!cabe && umbral > 0);

			if (!cabe) {
				bucle: for (const [f, fila] of this.matriz.entries()) {
					for (const [c, columna] of fila.entries()) {
						for (const dir of Punto.DIRECCIONES) {
							punto.x = c;
							punto.y = f;
							direccion = dir;
							if (columna.letra == letras[0] || columna.letra == "") {
								cabe = this.probar(letras, punto, direccion);
								if (cabe) break bucle;
							}
						}
					}
				}
			}

			if (cabe) {
				this.llenar(letras, punto, direccion);
				this.palabras.set(letras, palabra);
			} else {
				excedentes.espacio.push(palabra);
			}
		});

		if (excedentes.largo.length) {
			console.info("Estas palabras son muy largas", excedentes.largo);
		}
		if (excedentes.espacio.length) {
			console.info("Estas palabras no caben", excedentes.espacio);
		}

		this.palabras = new Map(
			[...this.palabras.entries()].sort((a, b) => a[0].localeCompare(b[0]))
		);

		this.matriz.forEach((fila) => {
			fila.forEach((columna) => {
				if (columna.letra == "") {
					columna.letra = _.sample(Sopa.LETRAS);
				}
			});
		});
	}

	probar(palabra, punto, direccion) {
		const origen = new Punto(punto.x, punto.y);

		if (!this.area.estaDentro(origen.saltar(direccion, palabra.length))) {
			return false;
		}

		for (let letra of palabra) {
			const caracter = this.matriz[origen.y][origen.x].letra;
			if (caracter != "" && caracter != letra) {
				return false;
			}
			origen.mover(direccion);
		}

		return true;
	}

	llenar(palabra, punto, direccion) {
		const origen = new Punto(punto.x, punto.y);
		for (let letra of palabra) {
			this.matriz[origen.y][origen.x].letra = letra;
			this.matriz[origen.y][origen.x].palabras.push(palabra);
			origen.mover(direccion);
		}
	}

	normalizar(s) {
		var r = s.toLowerCase();
		r = r.replace(new RegExp(/\s/g), "");
		r = r.replace(new RegExp(/[àáâãäå]/g), "a");
		r = r.replace(new RegExp(/æ/g), "ae");
		r = r.replace(new RegExp(/ç/g), "c");
		r = r.replace(new RegExp(/[èéêë]/g), "e");
		r = r.replace(new RegExp(/[ìíîï]/g), "i");
		r = r.replace(new RegExp(/[òóôõö]/g), "o");
		r = r.replace(new RegExp(/œ/g), "oe");
		r = r.replace(new RegExp(/[ùúûü]/g), "u");
		r = r.replace(new RegExp(/[ýÿ]/g), "y");
		return r.toUpperCase();
	}

	seleccion(inicio, fin) {
		let letras = [];
		for (let p of inicio.recorrido(fin)) {
			letras.push(this.matriz[p.y][p.x].letra);
		}
		return letras.join("");
	}

	buscar(inicio, fin) {
		let palabra = this.seleccion(inicio, fin);
		if (this.palabras.has(palabra)) {
			return palabra;
		}

		palabra = palabra.reverse();

		if (this.palabras.has(palabra)) {
			return palabra;
		}

		return false;
	}
}
// fin clase Sopa

// Contruyo el juego
// const lista = [
// 	"kayak",
// 	"reconocer",
// 	"radar",
// 	"arenera",
// 	"somos",
// 	"gente",
// 	"hombre",
// 	"mujer",
// 	"mama",
// 	"papa",
// 	"club",
// 	"golf",
// 	"sala",
// 	"reacción",
// 	"ñusta",
// 	"ñandu",
// 	"ñaño",
// 	"niñez",
// 	"niña",
// 	"bosque",
// 	"selva",
// 	"jungla",
// 	"desierto",
// 	"costa",
// 	"playa",
// 	"laguna",
// 	"río",
// 	"mar",
// 	"duna",
// 	"océano",
// 	"cerro",
// 	"monte",
// 	"montaña",
// 	"ave",
// 	"asa",
// 	"oso",
// 	"zorro",
// 	"yoyo",
// 	"xilofono",
// 	"kilo",
// 	"quinua",
// 	"murcielago",
// 	"elefante",
// 	"jirafa",
// 	"conejo",
// 	"alpaca",
// 	"sapo",
// 	"rata",
// 	"vaca",
// 	"toro",
// 	"pato",
// 	"beso",
// 	"perro",
// 	"gato",
// 	"sol",
// 	"luna",
// 	"mira",
// 	"oye",
// 	"hipopotamo",
// 	"electroencefalografista",
// 	"Licencia",
// 	"Negocio",
// 	"Plan",
// 	"Perfil",
// 	"Capital",
// 	"balance",
// 	"cuenta",
// 	"diseño",
// 	"asesoria",
// 	"contabilidad",
// 	"papel",
// 	"taza",
// 	"huye",
// 	"dia",
// 	"noche",
// 	"boda",
// 	"nacer",
// 	"reir",
// 	"viaje",
// 	"billete",
// 	"oro"
// ];

const lista = [
	"Abundancia",
	"Achumani",
	"Alasita",
	"Bicicleta",
	"Caja Ferroviaria",
	"Chasquipampa",
	"Chikititi",
	"Ekeko",
	"Incallojeta",
	"Integradora",
	"Irpavi",
	"La Paz BUS",
	"La Portada",
	"Mascotas",
	"Objetos Perdidos",
	"Puma Futbolero",
	"Pumakatari",
	"Rutas",
	"SAC",
	"Suerte",
	"Tarjeta Inteligente",
	"Villa Salome"
];

// const ancho = _.random(8, 17);
// const alto = _.random(6, ancho);
const ancho = 20;
const alto = 20;

const sopa = new Sopa(ancho, alto, lista);

const h = html.h;
const c = css.c;

c(`#${cuadricula.id}`, {
	"grid-template-rows": `repeat(${alto}, var(--celda))`,
	"grid-template-columns": `repeat(${ancho}, var(--celda))`
});

const matriz = sopa.matriz;

let inicial = false;
let final = false;

matriz.forEach((fila, f) => {
	fila.forEach((columna, c) => {
		const actual = matriz[f][c];

		const palabras = actual.palabras.map((item) => _.deburr(item).toLowerCase());
		const clases = [`letra`, `px${c}`, `py${f}`, ...palabras];

		const letra = h(
			"div",
			h("span", actual.letra),
			{
				class: clases.join(" ").toLowerCase(),
				style: `grid-column: ${c + 1}; grid-row: ${f + 1};`,
				"data-palabras": palabras,
				"data-fila": f,
				"data-columna": c
			},
			{
				click: letraClick,
				tap: letraClick
			}
		);

		if (actual.palabras.length) {
			letra.classList.add("pista");
		}

		cuadricula.appendChild(letra);
	});
});

const items = [...sopa.palabras.entries()].sort((a, b) =>
	a[0].localeCompare(b[0])
);
for (const [clave, valor] of items) {
	const id = _.deburr(clave).toLowerCase();
	const palabra = _.capitalize(valor);

	const item = h(
		`li#palabra-${id}`,
		h("span", palabra),
		{
			"data-palabra": id
		},
		{
			click: (evt) => {
				if (evt.ctrlKey && evt.altKey) {
					const selector = "#cuadricula ." + evt.currentTarget.dataset.palabra;
					gsap.set(selector, {
						outline: "5px solid red"
					});
					gsap.to(selector, {
						duration: 1,
						outline: "1px solid transparent"
					});
				}
			}
		}
	);

	palabras.appendChild(item);
}

function letraClick(evt) {
	const $letra = evt.currentTarget;
	const fila = $letra.dataset.fila;
	const columna = $letra.dataset.columna;

	if (evt.ctrlKey && evt.altKey) {
		const selector =
			"#cuadricula ." + $letra.dataset.palabras.split(",").join(",#cuadricula .");

		gsap.set(selector, {
			outline: "5px solid red"
		});
		gsap.to(selector, {
			duration: 1,
			outline: "1px solid transparent"
		});
	} else {
		jugar(fila, columna);
	}
}

let linea;
const total = _.size(sopa.palabras);
let puntos = 0;

function jugar(fila, columna) {
	if (!inicial) {
		inicial = new Punto(columna, fila);
		linea = h(".linea");
		cuadricula.appendChild(linea);

		const x = `calc( (var(--celda) + var(--espacio)) * ${columna} )`;
		const y = `calc( (var(--celda) + var(--espacio)) * ${fila} )`;

		gsap.set(linea, {
			left: x,
			top: y,
			width: `calc(var(--celda))`,
			transform: `rotate(0deg)`
		});

		gsap.fromTo(linea, { opacity: 0 }, { opacity: 1, duration: 0.5 });
	} else {
		punto = new Punto(columna, fila);
		const angulo = inicial.angulo(punto);

		if (angulo % 45 == 0) {
			final = punto;

			const tl = new TimelineMax();

			const distancia = inicial.distancia(final);

			const largo = `calc( (var(--celda) * ${1 + distancia}) 
				+ (var(--espacio) * ${distancia}) )`;
			const giro = `rotate(${angulo}deg)`;

			tl.fromTo(
				linea,
				{
					width: `calc(var(--celda))`,
					transform: giro
				},
				{
					duration: 0.3,
					width: largo
				}
			);

			const encontrada = sopa.buscar(inicial, final);
			if (encontrada) {
				const id = _.deburr(encontrada).toLowerCase();
				document.querySelector(`#palabra-${id}`).classList.add("encontrada");

				tl.to(linea, {
					backgroundColor: "limegreen",
					duration: 0.2
				});
				tl.to(
					linea,
					{
						backgroundColor: "transparent",
						duration: 0.1
					},
					"+=0.1"
				);
				tl.set(linea, {
					className: "linea correcta"
					// delay: 0.5
				});

				puntos += 1;

				puntaje.querySelector(".valor").textContent = puntos;
				if (puntos == total) {
					// alert("Has ganado");
					tl.call(
						() => {
							console.info("Has ganado");
							alert("Has ganado");
						},
						null,
						"+=0.5"
					);
				}
			} else {
				tl.set(linea, {
					className: "linea incorrecta"
				});
				tl.to(linea, {
					alpha: 0,
					duration: 0.5,
					delay: 0.3,
					onComplete: () => {
						linea.remove();
						linea = null;
					}
				});
			}

			inicial = false;
			final = false;
		}
	}
}

const coloresPalabras = new Map();

function obtenerColorePalabra(palabra) {
	if (coloresPalabras.has(palabra)) {
		return coloresPalabras.get(palabra);
	}

	const color = new Color("white").to("hsl");
	color.hsl.h = _.random(0, 360);
	color.hsl.s = _.random(25, 100);
	color.hsl.l = _.random(25, 75);

	coloresPalabras.set(palabra, color);
	return color;
}

keyboardJS.bind("ctrl + alt > k", (e) => {
	const ll = "ABCDEF";
	cuadricula.querySelectorAll(".letra:not(.pista)").forEach((item) => {
		item.classList.toggle("ignorada");
	});
	cuadricula.querySelectorAll(".pista").forEach((item) => {
		item.classList.toggle("resaltada");
		if (item.classList.contains("resaltada")) {
			const p = item.dataset.palabras;

			const palabras = item.dataset.palabras.split(",");

			color = new Color("white");

			let indice = 0.9;
			palabras.forEach((palabra) => {
				nuevo = obtenerColorePalabra(palabra);
				color = color.mix(nuevo, indice, { space: "lch", outputSpace: "hsl" });
				indice -= 0.2;
			});

			item.style.backgroundColor = color;

			let onWhite = Math.abs(color.contrast("white", "APCA"));
			let onBlack = Math.abs(color.contrast("black", "APCA"));
			item.style.color = onWhite > onBlack ? "white" : "black";
		} else {
			item.style.backgroundColor = "";
			item.style.color = "";
		}
	});
});

let zoom = 1;

keyboardJS.bind("ctrl + shift > up", (e) => {
	cambiarZoom(0.5);
});

keyboardJS.bind("ctrl + shift > down", (e) => {
	cambiarZoom(-0.5);
});

keyboardJS.bind("ctrl + alt > up", (e) => {
	cambiarZoom(0.05);
});

keyboardJS.bind("ctrl + alt > down", (e) => {
	cambiarZoom(-0.05);
});

function cambiarZoom(valor) {
	zoom = _.clamp(zoom + valor, 0.5, 4);
	console.debug("zoom", zoom, `scale(${zoom});`);
	juego.style.transform = `scale(${zoom})`;
}

              
            
!
999px

Console