mirroring poetry
Playground for ouilipo poetry that can be read in a mirror - also at https://mirroringpoetry.art/
boardwordsContainermirrorBoardboardPaddinginitializeWordsshuffledWordsselectedWordsremainingWordswordElementaddEventListenerscreateWordElementshuffleArrayhandleDropnewWordwordIndexdownloadBoardAsImagewordsurllinkdownloadMirroredBoardAsImagemirrorBoardContainerresetBoardaddButtonListenersbutton
document.addEventListener("DOMContentLoaded", () => {
const board = document.getElementById("board");
const wordsContainer = document.getElementById("words");
const mirrorBoard = document.getElementById("mirror-board");
const boardPadding =
parseFloat(window.getComputedStyle(board).paddingLeft) / 2 - 6;
let { selectedWords, remainingWords } = initializeWords(wordsContainer, 15);
addEventListeners(
board,
boardPadding,
mirrorBoard,
wordsContainer,
selectedWords,
remainingWords
);
});
const initializeWords = (wordsContainer, count) => {
const shuffledWords = shuffleArray(words);
const selectedWords = shuffledWords.slice(0, count);
const remainingWords = shuffledWords.slice(count);
selectedWords.forEach((word) => {
const wordElement = createWordElement(word, remainingWords, selectedWords);
wordsContainer.appendChild(wordElement);
});
return { selectedWords, remainingWords };
};
const addEventListeners = (
board,
boardPadding,
mirrorBoard,
wordsContainer,
selectedWords,
remainingWords
) => {
board.addEventListener("dragover", (event) => event.preventDefault());
board.addEventListener("drop", (event) =>
handleDrop(
event,
boardPadding,
board,
mirrorBoard,
wordsContainer,
selectedWords,
remainingWords
)
);
addButtonListeners("download-button", () => downloadBoardAsImage(board));
addButtonListeners("reset-button", () => {
resetBoard(board, mirrorBoard);
({ selectedWords, remainingWords } = initializeWords(wordsContainer, 15));
});
addButtonListeners("download-button2", downloadMirroredBoardAsImage);
};
const createWordElement = (
text,
remainingWords,
selectedWords,
isOnBoard = false
) => {
const wordElement = document.createElement("div");
wordElement.classList.add("word");
wordElement.textContent = text;
wordElement.setAttribute("draggable", "true");
wordElement.style.zIndex = 10;
wordElement.addEventListener("dragstart", handleDragStart);
wordElement.addEventListener("dragend", handleDragEnd);
if (!isOnBoard) {
wordElement.addEventListener("dblclick", () =>
handleDoubleClickInPool(wordElement, remainingWords, selectedWords)
);
} else {
wordElement.addEventListener("dblclick", () => {
removeWordElement(wordElement);
updateMirrorBoard(
document.getElementById("board"),
document.getElementById("mirror-board-container")
);
});
}
addTouchEventListeners(wordElement, remainingWords, selectedWords, isOnBoard);
return wordElement;
};
const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
};
const handleDrop = (
event,
boardPadding,
board,
mirrorBoard,
wordsContainer,
selectedWords,
remainingWords
) => {
event.preventDefault();
const data = JSON.parse(event.dataTransfer.getData("text/plain"));
const { text, offsetX, offsetY } = data;
const { left, top } = board.getBoundingClientRect();
const wordElement = createWordElement(
text,
remainingWords,
selectedWords,
true
);
wordElement.style.position = "absolute";
wordElement.style.left = `${event.clientX - left - offsetX - boardPadding}px`;
wordElement.style.top = `${event.clientY - top - offsetY - boardPadding}px`;
board.appendChild(wordElement);
updateMirrorBoard(board, mirrorBoard);
if (remainingWords.length > 0) {
const newWord = remainingWords.shift();
const wordIndex = selectedWords.indexOf(text);
if (wordIndex !== -1) {
selectedWords[wordIndex] = newWord;
wordsContainer.children[wordIndex].textContent = newWord;
}
}
wordElement.addEventListener("dblclick", () => {
removeWordElement(wordElement);
updateMirrorBoard(board, mirrorBoard);
});
};
const downloadBoardAsImage = (boardElement) => {
const words = boardElement.querySelectorAll(".word");
words.forEach((word) => (word.style.zIndex = 1));
html2canvas(boardElement, { scale: 10 })
.then((canvas) => {
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "poem.png";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
});
})
.catch(console.error);
words.forEach((word) => (word.style.zIndex = 10));
};
const downloadMirroredBoardAsImage = () => {
const mirrorBoardContainer = document.getElementById(
"mirror-board-container"
);
html2canvas(mirrorBoardContainer, { scale: 10 })
.then((canvas) => {
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "mirror_poem.png";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
});
})
.catch(console.error);
};
const resetBoard = (boardElement, mirrorBoard) => {
while (boardElement.firstChild) {
boardElement.removeChild(boardElement.firstChild);
}
const wordsContainer = document.getElementById("words");
while (wordsContainer.firstChild) {
wordsContainer.removeChild(wordsContainer.firstChild);
}
updateMirrorBoard(boardElement, mirrorBoard);
};
const addButtonListeners = (buttonId, callback) => {
const button = document.getElementById(buttonId);
if (button) {
button.addEventListener("click", callback);
button.addEventListener("touchend", (event) => {
event.preventDefault();
callback();
});
}
};
const handleDragStart = (event) => {
const rect = event.target.getBoundingClientRect();
const offsetX = event.clientX - rect.left;
const offsetY = event.clientY - rect.top;
event.dataTransfer.setData(
"text/plain",
JSON.stringify({ text: event.target.innerText, offsetX, offsetY })
);
event.target.style.zIndex = 1000;
event.target.classList.add("dragging");
};
const handleDragEnd = (event) => {
event.target.style.zIndex = 10;
event.target.classList.remove("dragging");
};
const handleDoubleClickInPool = (
wordElement,
remainingWords,
selectedWords
) => {
const parentElement = wordElement.parentElement;
if (
parentElement &&
parentElement.id === "words" &&
remainingWords.length > 0
) {
const oldWord = wordElement.textContent;
const newWord = remainingWords.shift();
wordElement.textContent = newWord;
remainingWords.push(oldWord);
const wordIndex = Array.from(parentElement.children).indexOf(wordElement);
if (wordIndex !== -1) {
selectedWords[wordIndex] = newWord;
}
}
};
const addTouchEventListeners = (
wordElement,
remainingWords,
selectedWords,
isOnBoard
) => {
let lastTap = 0;
wordElement.addEventListener("touchstart", (event) =>
handleTouchStart(event, wordElement, isOnBoard)
);
wordElement.addEventListener("touchmove", handleTouchMove);
wordElement.addEventListener("touchend", (event) =>
handleTouchEnd(event, wordElement, remainingWords, selectedWords, isOnBoard)
);
if (isOnBoard) {
wordElement.addEventListener("touchend", (event) => {
const currentTime = new Date().getTime();
const tapLength = currentTime - lastTap;
if (tapLength < 500 && tapLength > 0) {
removeWordElement(wordElement);
updateMirrorBoard(
document.getElementById("board"),
document.getElementById("mirror-board")
);
}
lastTap = currentTime;
});
} else {
wordElement.addEventListener("touchend", (event) => {
const currentTime = new Date().getTime();
const tapLength = currentTime - lastTap;
if (tapLength < 500 && tapLength > 0) {
handleDoubleClickInPool(wordElement, remainingWords, selectedWords);
}
lastTap = currentTime;
});
}
};
const handleTouchStart = (event, wordElement, isOnBoard) => {
const touch = event.targetTouches[0];
const offsetX = touch.clientX - event.target.getBoundingClientRect().left;
const offsetY = touch.clientY - event.target.getBoundingClientRect().top;
wordElement.__offsetX = offsetX;
wordElement.__offsetY = offsetY;
wordElement.__initialTouchX = touch.clientX;
wordElement.__initialTouchY = touch.clientY;
wordElement.__cloneCreated = false;
wordElement.__singleTap = true;
if (isOnBoard) {
event.preventDefault();
}
};
const handleTouchMove = (event) => {
event.preventDefault();
const touch = event.targetTouches[0];
const original = event.target;
if (
!original.__cloneCreated &&
(Math.abs(touch.clientX - original.__initialTouchX) > 10 ||
Math.abs(touch.clientY - original.__initialTouchY) > 10)
) {
const clone = original.cloneNode(true);
clone.classList.add("dragging");
clone.style.position = "absolute";
clone.style.zIndex = 100;
clone.style.pointerEvents = "none";
clone.style.left = `${touch.pageX - original.__offsetX}px`;
clone.style.top = `${touch.pageY - original.__offsetY}px`;
document.body.appendChild(clone);
original.__clone = clone;
original.__cloneCreated = true;
original.__singleTap = false;
}
if (original.__cloneCreated) {
const clone = original.__clone;
clone.style.left = `${touch.pageX - original.__offsetX}px`;
clone.style.top = `${touch.pageY - original.__offsetY}px`;
}
};
const handleTouchEnd = (
event,
wordElement,
remainingWords,
selectedWords,
isOnBoard
) => {
const original = event.target;
const clone = original.__clone;
if (original.__singleTap) {
return;
}
if (clone) {
const board = document.getElementById("board");
const { left, top } = board.getBoundingClientRect();
const cloneRect = clone.getBoundingClientRect();
const dropX = cloneRect.left - left - 3;
const dropY = cloneRect.top - top - 3;
let x = dropX;
let y = dropY;
const wordWidth = clone.offsetWidth;
const wordHeight = clone.offsetHeight;
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x + wordWidth > board.clientWidth) x = board.clientWidth - wordWidth;
if (y + wordHeight > board.clientHeight)
y = board.clientHeight - wordHeight;
if (
x >= 0 &&
y >= 0 &&
x + wordWidth <= board.clientWidth &&
y + wordHeight <= board.clientHeight
) {
const newWordElement = createWordElement(
original.innerText,
remainingWords,
selectedWords,
true
);
newWordElement.style.position = "absolute";
newWordElement.style.left = `${x}px`;
newWordElement.style.top = `${y}px`;
board.appendChild(newWordElement);
updateMirrorBoard(board, document.getElementById("mirror-board"));
if (original.parentElement.id === "words" && remainingWords.length > 0) {
const newWord = remainingWords.shift();
const wordIndex = Array.from(original.parentElement.children).indexOf(
original
);
if (wordIndex !== -1) {
original.parentElement.children[wordIndex].textContent = newWord;
selectedWords[wordIndex] = newWord;
}
}
}
clone.remove();
original.__cloneCreated = false;
}
if (isOnBoard) {
const currentTime = new Date().getTime();
const tapLength = currentTime - original.__lastTapTime;
if (tapLength < 500 && tapLength > 0) {
removeWordElement(wordElement);
updateMirrorBoard(
document.getElementById("board"),
document.getElementById("mirror-board")
);
}
original.__lastTapTime = currentTime;
}
};
const removeWordElement = (wordElement) => {
if (wordElement.parentElement) {
wordElement.parentElement.removeChild(wordElement);
}
};
const updateMirrorBoard = (board, mirrorBoard) => {
mirrorBoard.innerHTML = "";
Array.from(board.children).forEach((child) => {
const clonedChild = child.cloneNode(true);
mirrorBoard.appendChild(clonedChild);
});
const originalStyles = window.getComputedStyle(board);
for (let style of originalStyles) {
mirrorBoard.style[style] = originalStyles[style];
}
const draggedElement = mirrorBoard.querySelector(".dragging");
if (draggedElement) {
draggedElement.classList.remove("dragging");
}
};