Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
027946444This file is pinned to the selected release. Download the original to open it in your design tool.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flipdot Canvas</title>
<style>
h1 {
font-size: 3vw;
}
body {
background: rgb(210, 210, 210);
font-family: Arial, sans-serif;
text-align: center;
}
.center {
display: flex;
justify-content: center;
}
.grid {
display: grid;
grid-template-columns: repeat(21, 4vw);
gap: 2px;
touch-action: none;
background: #000000;
padding: 0.8vw;
margin: 1.5vw;
border-radius: 1vw;
}
.cell-wrapper {
width: 4vw;
height: 4vw;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.cell {
width: 100%;
height: 100%;
background: rgb(40, 40, 40);
clip-path: polygon(
var(--corner) 0%,
calc(100% - var(--corner)) 0%,
100% var(--corner),
100% calc(100% - var(--corner)),
calc(100% - var(--corner)) 100%,
var(--corner) 100%,
0% calc(100% - var(--corner)),
0% var(--corner)
);
--corner: 0.6vw;
pointer-events: none; /* important so clicks pass through to wrapper */
}
.cell.on {
background: yellow;
}
button {
margin: 0.8vw;
padding: 0.8vw;
font-size: 1.5vw;
}
textarea {
font-size: 1vw;
margin-top: 1vw;
}
@media (pointer:none), (pointer:coarse) {
h1 {
font-size: 24px;
}
.grid {
gap: 1px;
}
button {
font-size: 2vw;
}
}
</style>
</head>
<body>
<h1>Flipdot Canvas</h1>
<a href="/" style="text-decoration:none">
<button id="backButton">Back</button>
</a>
<button id="modeToggle">Mode: Draw</button>
<button id="all-on">All On</button>
<button id="all-off">All Off</button>
<button id="import">Import</button>
<button id="stream">Stream: Inactive</button>
<button id="send">Send</button>
<div class="center" oncontextmenu="return false;">
<div class="grid" id="canvas"></div>
</div>
<textarea id="inputOutputBox" rows="1" cols="100" placeholder="Input/output area (e.g., [24,36,...])"></textarea>
<script>
const rows = 7, cols = 21;
let drawMode = true; // true -> drawing, false -> erasing
let isDrawing = false;
let socket = {readyState: WebSocket.CLOSED};
let lastToggle = [-1,-1]; // (row, col)
const canvas = document.getElementById("canvas");
const modeToggle = document.getElementById("modeToggle");
const allOnBtn = document.getElementById("all-on");
const allOffBtn = document.getElementById("all-off");
const importBtn = document.getElementById("import");
const streamBtn = document.getElementById("stream");
const sendBtn = document.getElementById("send");
const grid = Array.from({ length: rows }, () => Array(cols).fill(false));
const inputOutputBox = document.getElementById("inputOutputBox");
function toggleWebSocket() {
if (socket instanceof WebSocket && socket.readyState === WebSocket.OPEN) {
socket.close();
streamBtn.textContent = "Stream: Inactive";
} else {
socket = new WebSocket(`ws://${window.location.host}/ws`);
socket.onopen = () => {
console.log("WebSocket connected");
socket.send(JSON.stringify({"frame": getCanvasBitwise()}));
streamBtn.textContent = "Stream: Active";
};
socket.onmessage = (event) => { // not used but maybe useful
const data = JSON.parse(event.data);
if (data.frame) {
for (let c = 0; c < cols; c++) {
for (let r = 0; r < rows; r++) {
grid[r][c] = (data.frame[c] >> r) & 1;
}
}
renderGrid();
} else if (data.dot) {
forceCell(data.dot[0], data.dot[1], data.dot[2]);
}
};
socket.onclose = () => {
console.log("WebSocket disconnected");
streamBtn.textContent = "Stream: Inactive";
};
socket.onerror = (error) => {
console.error("WebSocket error:", error);
streamBtn.textContent = "Stream: Inactive";
};
}
}
function renderGrid() {
canvas.innerHTML = "";
grid.forEach((row, rIdx) => {
row.forEach((cell, cIdx) => {
const wrapper = document.createElement("div");
wrapper.classList.add("cell-wrapper");
const visual = document.createElement("div");
visual.classList.add("cell");
wrapper.appendChild(visual);
if (cell) visual.classList.add("on");
wrapper.addEventListener("mousedown", (e) => {
isDrawing = true;
drawMode = (e.button >= 1) ? false : true;
modeToggle.textContent = `Mode: ${drawMode ? "Draw" : "Erase"}`;
toggleCell(rIdx, cIdx);
});
wrapper.addEventListener("mouseup", (e) => {
isDrawing = false;
});
wrapper.addEventListener("mouseover", (e) => {
if (isDrawing && (rIdx !== lastToggle[0] || cIdx !== lastToggle[1]) && drawMode !== grid[rIdx][cIdx]) {
toggleCell(rIdx, cIdx);
}
});
wrapper.addEventListener("touchstart", (e) => {
e.preventDefault();
isDrawing = true;
toggleCell(rIdx, cIdx);
});
wrapper.addEventListener("touchmove", (e) => {
e.preventDefault();
const touch = e.touches[0];
const element = document.elementFromPoint(touch.clientX, touch.clientY);
if (element && element.classList.contains("cell-wrapper")) {
const index = Array.from(canvas.children).indexOf(element);
const row = Math.floor(index / cols);
const col = index % cols;
if ((row !== lastToggle[0] || col !== lastToggle[1]) && drawMode !== grid[row][col]) toggleCell(row, col);
}
});
canvas.appendChild(wrapper);
});
});
}
function toggleCell(r, c) {
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({"dot": [c,r,drawMode ? 1 : 0]}));
grid[r][c] = drawMode ? true : false;
lastToggle = [r,c];
renderGrid();
}
function forceCell(r, c, state) {
grid[r][c] = (state>0)? true : false;
renderGrid();
}
document.addEventListener("mouseup", () => isDrawing = false);
document.addEventListener("touchend", () => isDrawing = false);
modeToggle.addEventListener("click", () => {
drawMode = !drawMode;
modeToggle.textContent = `Mode: ${drawMode ? "Draw" : "Erase"}`;
});
allOnBtn.addEventListener("click", () => {
grid.forEach((row, rIdx) => {
row.forEach((cell, cIdx) => {
grid[rIdx][cIdx] = true;
});
});
renderGrid();
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({"frame": getCanvasBitwise()}));
});
allOffBtn.addEventListener("click", () => {
grid.forEach((row, rIdx) => {
row.forEach((cell, cIdx) => {
grid[rIdx][cIdx] = false;
});
});
renderGrid();
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({"frame": getCanvasBitwise()}));
});
streamBtn.addEventListener("click", () => {
toggleWebSocket();
});
function getCanvasBitwise() {
const frameOut = Array(cols).fill(0);
for (let c = 0; c < cols; c++) {
for (let r = 0; r < rows; r++) {
frameOut[c] |= grid[r][c] << r;
}
}
return frameOut;
}
sendBtn.addEventListener("click", () => {
const frameOut = getCanvasBitwise();
inputOutputBox.value = JSON.stringify(frameOut);
response = fetch(window.location.origin + '/mode', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({'mode': 'Static', 'params': {'frame': frameOut}})
});
});
importBtn.addEventListener("click", () => {
let frameIn;
try {
frameIn = JSON.parse(inputOutputBox.value);
if (!Array.isArray(frameIn)) {
throw new Error("Invalid format");
}
for (let c = 0; c < Math.min(cols, frameIn.length); c++) {
for (let r = 0; r < rows; r++) {
grid[r][c] = ((frameIn[c] >> r) & 1)? true : false;
}
}
renderGrid();
} catch (err) {
inputOutputBox.value = "Error importing: " + err.message;
}
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({"frame": getCanvasBitwise()}));
});
renderGrid();
</script>
</body>
</html>