This page lists files in the current directory. You can view content, get download/execute commands for Wget, Curl, or PowerShell, or filter the list using wildcards (e.g., `*.sh`).
wget 'https://sme10.lists2.roe3.org/mdrone/ccsgen/index.html'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Christmas Cross-Stitch Message Generator</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<main>
<header>
<h1>Christmas Cross-Stitch Message Generator</h1>
<div class="controls">
<textarea id="message" placeholder="Write your Christmas message…" size="40" ></textarea>
<button type="button" id="btn-generate">Generate</button>
<button type="button" id="btn-clear">Clear</button>
</div>
</header>
<div id="stitches" class="stitches"></div>
</main>
<script src="./script.js"></script>
<br><br><br><br>
<p style="color:#ffffff;">(Hint: To save the result, take a screenshot and use a graphics program to crop the image)</p>
</body>
</html>
wget 'https://sme10.lists2.roe3.org/mdrone/ccsgen/script.js'
console.clear();
const FIRST_WORD = "Merry Christmas";
const CHAR_BITMAPS = {
"A": [
"0220",
"2002",
"2002",
"2222",
"2002",
"2002"
],
"B": [
"2220",
"2002",
"2220",
"2002",
"2002",
"2220"
],
"C": [
"0220",
"2002",
"2000",
"2000",
"2002",
"0220"
],
"D": [
"2220",
"2002",
"2002",
"2002",
"2002",
"2220"
],
"E": [
"2222",
"2000",
"2220",
"2000",
"2000",
"2222"
],
"F": [
"2222",
"2000",
"2220",
"2000",
"2000",
"2000"
],
"G": [
"0220",
"2002",
"2000",
"2022",
"2002",
"0220"
],
"H": [
"2002",
"2002",
"2222",
"2002",
"2002",
"2002"
],
"I": [
"222",
"020",
"020",
"020",
"020",
"222"
],
"J": [
"2222",
"0020",
"0020",
"0020",
"2020",
"0200"
],
"K": [
"2002",
"2020",
"2200",
"2020",
"2002",
"2002"
],
"L": [
"200",
"200",
"200",
"200",
"200",
"222"
],
"M": [
"20002",
"22022",
"20202",
"20002",
"20002",
"20002"
],
"N": [
"2002",
"2202",
"2022",
"2002",
"2002",
"2002"
],
"O": [
"0220",
"2002",
"2002",
"2002",
"2002",
"0220"
],
"P": [
"2220",
"2002",
"2220",
"2000",
"2000",
"2000"
],
"Q": [
"0220",
"2002",
"2002",
"2002",
"2022",
"0220"
],
"R": [
"2220",
"2002",
"2220",
"2002",
"2002",
"2002"
],
"S": [
"0220",
"2002",
"0220",
"0002",
"2002",
"0220"
],
"T": [
"222",
"020",
"020",
"020",
"020",
"020"
],
"U": [
"2002",
"2002",
"2002",
"2002",
"2002",
"0220"
],
"V": [
"2002",
"2002",
"2002",
"2002",
"0220",
"0200"
],
"W": [
"20002",
"20002",
"20002",
"20202",
"22022",
"20002"
],
"X": [
"2002",
"2002",
"0220",
"2002",
"2002",
"2002"
],
"Y": [
"2002",
"2002",
"0222",
"0002",
"2002",
"0220"
],
"Z": [
"2222",
"0002",
"0020",
"0200",
"2000",
"2222"
],
"0": [
"0220",
"2002",
"2002",
"2002",
"2002",
"0220"
],
"1": [
"220",
"020",
"020",
"020",
"020",
"222"
],
"2": [
"0220",
"2002",
"0020",
"0200",
"2000",
"2222"
],
"3": [
"2220",
"0002",
"0220",
"0002",
"0002",
"2220"
],
"4": [
"2002",
"2002",
"2222",
"0002",
"0002",
"0002"
],
"5": [
"2222",
"2000",
"2220",
"0002",
"0002",
"2220"
],
"6": [
"0220",
"2000",
"2220",
"2002",
"2002",
"0220"
],
"7": [
"2222",
"0002",
"0020",
"0200",
"0200",
"0200"
],
"8": [
"0220",
"2002",
"0220",
"2002",
"2002",
"0220"
],
"9": [
"0220",
"2002",
"2002",
"0222",
"0002",
"0220"
],
" ": [
"0000",
"0000",
"0000",
"0000",
"0000",
"0000"
],
"'": [
"2",
"2",
"0",
"0",
"0",
"0"
],
",": [
"0",
"0",
"0",
"0",
"2",
"2"
],
".": [
"0",
"0",
"0",
"0",
"0",
"2"
],
"!": [
"2",
"2",
"2",
"2",
"0",
"2"
],
"?": [
"0220",
"2002",
"0002",
"0020",
"0000",
"0020"
],
"¿": [
"0200",
"0000",
"0200",
"2000",
"2002",
"0220"
],
"@": [
"02220",
"20002",
"20222",
"20202",
"20022",
"02220"
],
"#": [
"02020",
"22222",
"02020",
"22222",
"02020",
"02020"
],
"-": [
"000",
"000",
"222",
"000",
"000",
"000"
]
};
const CHRISTMAS_SYMBOLS = {
"STAR": [
"20202",
"02220",
"22222",
"02220",
"20202"
],
"TREE": [
"00200",
"02220",
"22222",
"00200",
"00200"
],
"BELL": [
"00200",
"02220",
"02220",
"22222",
"00200"
],
"CANDY": [
"00200",
"02220",
"22222",
"02220",
"00200"
]
};
// config
const SETTINGS = {
GRID_COLS: 149,
GRID_ROWS: 50, // initial value - will expand if needed
MSG_CHAR_HEIGHT: 7,
MSG_SPACING: 1,
MSG_SPACE_WIDTH: 1,
VERTICAL_MESSAGE_PADDING: 6, // empty rows between strips and message
STRIP_PADDING_ROWS: 2,
STRIP_HORIZONTAL_PADDING: 1,
STRIP_BETWEEN_SYMBOLS: 1,
STRIP_DASHED: true,
STRIP_COLOR: "checkbox-white",
COLOR_CLASSES: ["checkbox-red", "checkbox-white"],
ALLOWED_MESSAGE_CHARS: "A-Z0-9 ,.'¡!?¿@#\\-"
};
// selectors
const grid = document.getElementById("stitches");
const btnGenerate = document.getElementById("btn-generate");
const btnClear = document.getElementById("btn-clear");
const messageInput = document.getElementById("message");
// set CSS variable for grid columns
grid.style.setProperty("--grid-cols", SETTINGS.GRID_COLS);
// create checkbox grid
let checkboxes = [];
function createGrid(rows) {
grid.innerHTML = "";
checkboxes = [];
for (let i = 0; i < SETTINGS.GRID_COLS * rows; i++) {
const cb = document.createElement("input");
cb.type = "checkbox";
grid.appendChild(cb);
checkboxes.push(cb);
}
grid.style.setProperty("--grid-rows", rows);
}
createGrid(SETTINGS.GRID_ROWS);
// function - draw a symbol at a given position
// ------------------------------
function drawSymbolAt(pattern, startX, startY) {
const height = pattern.length;
const width = pattern[0].length;
for (let y = 0; y < height; y++) {
const row = pattern[y] || "";
for (let x = 0; x < width; x++) {
if (row[x] === "2") {
const gx = startX + x;
const gy = startY + y;
if (gx >= 0 && gx < SETTINGS.GRID_COLS && gy >= 0 && gy < SETTINGS.GRID_ROWS) {
const idx = gy * SETTINGS.GRID_COLS + gx;
checkboxes[idx].checked = true;
checkboxes[idx].className = SETTINGS.COLOR_CLASSES[(x + y) % SETTINGS.COLOR_CLASSES.length];
}
}
}
}
}
// function - draw a full stitch line
function drawStitchLine(y, dashed = false, colorClass = SETTINGS.STRIP_COLOR) {
if (y < 0 || y >= SETTINGS.GRID_ROWS) return;
const step = dashed ? 2 : 1;
for (let x = 0; x < SETTINGS.GRID_COLS; x += step) {
const idx = y * SETTINGS.GRID_COLS + x;
checkboxes[idx].checked = true;
checkboxes[idx].className = colorClass;
}
}
// function - draw top/bottom strips
function drawSymbolStrip(options = {}) {
const {
paddingRows = SETTINGS.STRIP_PADDING_ROWS,
horizontalPadding = SETTINGS.STRIP_HORIZONTAL_PADDING,
betweenSymbols = SETTINGS.STRIP_BETWEEN_SYMBOLS,
dashed = SETTINGS.STRIP_DASHED,
stitchColor = SETTINGS.STRIP_COLOR
} = options;
const keys = Object.keys(CHRISTMAS_SYMBOLS);
if (!keys.length) return { topReserved: 0, bottomReserved: 0 };
// pick two distinct symbols
let a = Math.floor(Math.random() * keys.length);
let b;
do { b = Math.floor(Math.random() * keys.length); } while (b === a && keys.length > 1);
const symbolA = CHRISTMAS_SYMBOLS[keys[a]];
const symbolB = CHRISTMAS_SYMBOLS[keys[b]] || symbolA;
const stripHeight = Math.max(symbolA.length, symbolB.length);
// Draw a single strip at given stitchTopRow
function drawStripAt(stitchTopRow) {
const symbolsStartRow = stitchTopRow + 1 + paddingRows;
const symbolsBottomRow = symbolsStartRow + stripHeight - 1;
const stitchBottomRow = symbolsBottomRow + paddingRows + 1;
drawStitchLine(stitchTopRow, dashed, stitchColor);
// compute symbols layout
const patterns = [];
for (let i = 0; i < Math.ceil(SETTINGS.GRID_COLS / 2); i++) patterns.push(i % 2 === 0 ? symbolA : symbolB);
// place as many as fit
const placed = [];
let totalWidth = 0;
for (let i = 0; i < patterns.length; i++) {
const w = patterns[i][0].length;
const newTotal = placed.length === 0 ? totalWidth + w : totalWidth + betweenSymbols + w;
if (newTotal + horizontalPadding * 2 > SETTINGS.GRID_COLS) break;
placed.push(patterns[i]);
totalWidth = newTotal;
}
const startX = Math.floor((SETTINGS.GRID_COLS - totalWidth) / 2);
let cursorX = startX;
for (let pat of placed) {
drawSymbolAt(pat, cursorX, symbolsStartRow);
cursorX += pat[0].length + betweenSymbols;
}
drawStitchLine(stitchBottomRow, dashed, stitchColor);
return { top: stitchTopRow, bottom: stitchBottomRow };
}
// top strip (stitchTopRow = paddingRows)
const topReservedRange = drawStripAt(paddingRows);
// bottom strip (calculate based on expanded GRID_ROWS)
const bottomStitchBottom = SETTINGS.GRID_ROWS - 1 - paddingRows;
const bottomStitchTop = bottomStitchBottom - (stripHeight + paddingRows * 2 + 1);
const bottomReservedRange = drawStripAt(bottomStitchTop);
const topReservedRows = topReservedRange.bottom + 1;
const bottomReservedRows = SETTINGS.GRID_ROWS - bottomReservedRange.top;
return { topReserved: topReservedRows, bottomReserved: bottomReservedRows };
}
// function - draw message on grid
function drawMessage(msg) {
// clear all checkboxes
checkboxes.forEach(cb => {
cb.checked = false;
cb.className = "";
});
// normalize message
msg = (msg || "")
.toUpperCase()
.replace(new RegExp(`[^${SETTINGS.ALLOWED_MESSAGE_CHARS}\n]`, "g"), "");
if (!msg) return;
// width of a character ---
function charWidthOf(ch) {
if (ch === " ") return SETTINGS.MSG_SPACE_WIDTH;
const pattern = CHAR_BITMAPS[ch];
return pattern && pattern[0] ? pattern[0].length : SETTINGS.MSG_SPACE_WIDTH;
}
// Ssplit into lines
const rawLines = msg.split(/\r?\n/);
// build lines array
const lines = [];
for (const rawLine of rawLines) {
const words = rawLine.split(" ").filter(Boolean);
let currentLine = [];
let currentWidth = 0;
for (const word of words) {
let wordWidth = 0;
for (const ch of word) wordWidth += charWidthOf(ch) + SETTINGS.MSG_SPACING;
wordWidth -= SETTINGS.MSG_SPACING; // remove extra spacing
const extraSpace = currentLine.length > 0 ? SETTINGS.MSG_SPACE_WIDTH + SETTINGS.MSG_SPACING : 0;
if (currentWidth + extraSpace + wordWidth > SETTINGS.GRID_COLS) {
if (currentLine.length > 0) lines.push(currentLine);
currentLine = [];
currentWidth = 0;
}
if (currentLine.length > 0) {
currentLine.push(" ");
currentWidth += extraSpace;
}
for (const ch of word) {
currentLine.push(ch);
currentWidth += charWidthOf(ch) + SETTINGS.MSG_SPACING;
}
currentWidth -= SETTINGS.MSG_SPACING;
}
if (currentLine.length > 0) lines.push(currentLine);
}
// estimate message height in rows
const totalMessageHeight = lines.length * SETTINGS.MSG_CHAR_HEIGHT;
const totalVerticalPadding = SETTINGS.VERTICAL_MESSAGE_PADDING * 2;
const messageBlockHeight = totalMessageHeight + totalVerticalPadding;
// determine required grid height
const stripMaxHeight = Math.max(...Object.values(CHRISTMAS_SYMBOLS).map(p => p.length));
const stripFullHeight = stripMaxHeight + SETTINGS.STRIP_PADDING_ROWS * 2 + 2; // top stitch + padding + symbols + padding + bottom stitch
const requiredHeight = messageBlockHeight + 2 * stripFullHeight;
// expand grid if necesarry
if (requiredHeight > SETTINGS.GRID_ROWS) {
const extraRows = requiredHeight - SETTINGS.GRID_ROWS;
SETTINGS.GRID_ROWS = requiredHeight;
for (let i = 0; i < extraRows * SETTINGS.GRID_COLS; i++) {
const cb = document.createElement("input");
cb.type = "checkbox";
grid.appendChild(cb);
checkboxes.push(cb);
}
}
// draw top & bottom strips
const reserved = drawSymbolStrip({
paddingRows: SETTINGS.STRIP_PADDING_ROWS,
horizontalPadding: SETTINGS.STRIP_HORIZONTAL_PADDING,
betweenSymbols: SETTINGS.STRIP_BETWEEN_SYMBOLS,
dashed: SETTINGS.STRIP_DASHED,
stitchColor: SETTINGS.STRIP_COLOR
});
const { topReserved, bottomReserved } = reserved;
// calculate vertical start for message (centered between strips + vertical padding)
const availableHeight = SETTINGS.GRID_ROWS - (topReserved + bottomReserved);
const startY = topReserved + SETTINGS.VERTICAL_MESSAGE_PADDING + Math.floor((availableHeight - messageBlockHeight) / 2);
// render each line
lines.forEach((line, lineIndex) => {
// calculate line width
let lineWidth = 0;
for (let t = 0; t < line.length; t++) {
lineWidth += line[t] === " " ? SETTINGS.MSG_SPACE_WIDTH : charWidthOf(line[t]);
if (t < line.length - 1) lineWidth += SETTINGS.MSG_SPACING;
}
// horizontal centering
let cursorX = Math.floor((SETTINGS.GRID_COLS - lineWidth) / 2);
const yBase = startY + lineIndex * SETTINGS.MSG_CHAR_HEIGHT;
for (let ti = 0; ti < line.length; ti++) {
const token = line[ti];
if (token === " ") {
cursorX += SETTINGS.MSG_SPACE_WIDTH;
} else {
const pattern = CHAR_BITMAPS[token] || [];
const w = charWidthOf(token);
for (let y = 0; y < SETTINGS.MSG_CHAR_HEIGHT; y++) {
const row = pattern[y] || "";
for (let x = 0; x < w; x++) {
if (row[x] === "2") {
const gx = cursorX + x;
const gy = yBase + y;
if (gx >= 0 && gx < SETTINGS.GRID_COLS && gy >= 0 && gy < SETTINGS.GRID_ROWS) {
const idx = gy * SETTINGS.GRID_COLS + gx;
checkboxes[idx].checked = true;
checkboxes[idx].className = ""; // plain color
}
}
}
}
cursorX += w;
}
if (ti < line.length - 1) cursorX += SETTINGS.MSG_SPACING;
}
});
}
// clear grid
function clearGrid() {
messageInput.value = "";
drawMessage(""); // draws strips only
}
// events
btnGenerate.addEventListener("click", () => drawMessage(messageInput.value));
btnClear.addEventListener("click", clearGrid);
// show first message
drawMessage(FIRST_WORD);
wget 'https://sme10.lists2.roe3.org/mdrone/ccsgen/style.css'
@import url(https://fonts.bunny.net/css?family=annie-use-your-telescope:400);
@layers base, demo;
@layer demo{
:root{
--clr-bg: rgb(3, 46, 21);
--clr-txt: red;
}
body {
background-color: var(--clr-bg);
color: var(--clr-txt);
}
main{
font-family: 'Annie Use Your Telescope', handwriting;
display: grid;
place-items: center;
grid-template-rows: auto 1fr;
width: min(100%, 900px);
gap: 2rem;
}
header{
width: 100%;
display: grid;
gap: .5rem;
& h1{
margin: 0;
text-wrap: balance;
line-height: 1.2;
font-size: clamp(1.2rem, 3.5vw + .04rem, 1.6rem);
}
}
.stitches {
width: 100%;
align-self: start;
display: grid;
grid-template-columns: repeat(var(--grid-cols,100), 1fr);
grid-template-rows: repeat(var(--grid-rows), 1fr);
gap: 1px;
background: rgb(255 255 255 / .15);
/*border: 1px solid rgb(0 0 0 / .15);*/
}
input[type="checkbox"] {
--clr-checked: gold;
appearance: none;
aspect-ratio:1;
margin: 0;
padding: 0;
border-radius: 0;
border: transparent;
position: relative;
background-color: var(--clr-bg);
/* Diagonal bars using ::before and ::after */
&:checked::before,
&:checked::after {
content: "";
position: absolute;
inset:0;
margin: auto;
width: 2px;
height: 100%;
rotate: 45deg;
background: var(--clr-checked);
box-shadow: 1px 0 1px 1px rgb(0 0 0 / .25);
}
/* Rotate the second bar to form the other diagonal */
&:checked::after {
transform: rotate(90deg);
}
&.checkbox-red {
--clr-checked: red;
}
&.checkbox-white {
--clr-checked: white;
}
}
.controls{
display: grid;
grid-template-columns: 3fr 1fr;
gap: .5rem;
& :where(textarea,button){
font-size: 1.2rem;
font-family: inherit;
color: white;
border-radius: 3px;
border: none;
transition-property: offset,background-color,color;
transition-timing-function: ease-in-out;
transition-duration: 150ms;
&:focus-visible{
outline: 1px dotted white;
outline-offset: 2px;
}
}
& button{
background: red;
&:hover{
background-color: rgb(130, 24, 26);
}
}
& textarea {
grid-row: span 2;
height: 90px;
line-height: 1.2;
padding: 1rem;
background-color: rgb(255 255 255 / .2);
resize: none;
&::placeholder{
color: #CCC;
font-style: italic;
}
}
}
}
@layer base{
*,::before,::after{
box-sizing: border-box;
}
body {
padding: 1rem;
display: grid;
place-items: center;
font-family: system-ui;
font-size: 1rem;
line-height: 1.4;
}
}