Skip to content

Instantly share code, notes, and snippets.

@doshiraki
Created December 16, 2025 13:32
Show Gist options
  • Select an option

  • Save doshiraki/359b42d39b7153afa118d4ff843620ba to your computer and use it in GitHub Desktop.

Select an option

Save doshiraki/359b42d39b7153afa118d4ff843620ba to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>2026 Calendar - High Density</title>
<style>
/* =========================================
[Base Style]
========================================= */
:root {
--font-family-base: "Helvetica Neue", Arial, "Hiragino Kaku Gothic ProN", "BIZ UDPGothic", Meiryo, sans-serif;
--color-text-main: #333333;
--color-text-sat: #00a0e9;
--color-text-sun: #e60012;
--color-text-week: #777777;
--color-bg: #f0f2f5;
}
body {
font-family: var(--font-family-base);
background-color: var(--color-bg);
margin: 0;
padding: 8px; /* 画面端の余白も最小限に */
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
box-sizing: border-box;
}
/* =========================================
[UI Components]
========================================= */
.control-panel {
margin-bottom: 10px;
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: center;
width: 100%;
}
.btn {
padding: 8px 12px;
font-size: 12px;
font-weight: bold;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
font-family: var(--font-family-base);
flex: 1 1 auto;
max-width: 120px;
}
.btn-svg { background-color: #e67e22; }
.btn-png { background-color: #00a0e9; }
.btn-print { background-color: #555; }
/* =========================================
[Paper Container]
========================================= */
.sheet {
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 10px; /* 内部パディングも削減 */
width: 100%;
max-width: 800px;
box-sizing: border-box;
border-radius: 6px;
}
#svg-container { width: 100%; height: auto; }
svg { display: block; width: 100%; height: auto; }
svg text { font-family: var(--font-family-base); }
/* =========================================
[Print Settings]
========================================= */
@media print {
body { background: none; padding: 0; display: block; }
.control-panel { display: none !important; }
.sheet { box-shadow: none; padding: 0; width: 100%; max-width: none; margin: 0; border-radius: 0; }
@page { size: A4 portrait; margin: 5mm; }
}
</style>
</head>
<body>
<div class="control-panel">
<button class="btn btn-svg" onclick="downloadAsSvg()">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
<span>SVG</span>
</button>
<button class="btn btn-png" onclick="downloadAsPng()">
<span>画像(PNG)</span>
</button>
<button class="btn btn-print" onclick="window.print()">
<span>印刷</span>
</button>
</div>
<div class="sheet" id="calendar-sheet">
<div id="svg-container"></div>
</div>
<script>
// ============================================================
// [Config] 休日データ
// ============================================================
const mapHolidays = {
"2026-01-01": "年始休暇", "2026-01-02": "年始休暇", "2026-01-12": "成人の日",
"2026-02-11": "建国記念の日", "2026-02-23": "天皇誕生日", "2026-03-20": "春分の日",
"2026-04-29": "昭和の日", "2026-05-04": "みどりの日", "2026-05-05": "こどもの日", "2026-05-06": "振替休日",
"2026-07-20": "海の日", "2026-08-11": "山の日", "2026-08-12": "夏季休暇",
"2026-08-13": "夏季休暇", "2026-08-14": "夏季休暇", "2026-09-21": "敬老の日",
"2026-09-22": "国民の休日", "2026-09-23": "秋分の日", "2026-10-12": "スポーツの日",
"2026-11-03": "文化の日", "2026-11-23": "勤労感謝の日", "2026-12-29": "年末休暇",
"2026-12-30": "年末休暇", "2026-12-31": "年末休暇"
};
// ============================================================
// [Class] SvgCalendarBuilder (High Density)
// ============================================================
class SvgCalendarBuilder {
constructor(year) {
this.year = year;
this.svgNS = "http://www.w3.org/2000/svg";
// --- Layout Constants (Limit Break) ---
this.wCell = 40;
this.hCell = 24; // 28 -> 24 (限界圧縮)
this.gapMonthX = 15;
this.gapMonthY = 10; // 30 -> 10 (ほぼ隙間なし)
this.hTitleArea = 45; // 60 -> 45
// --- Font Styles (サイズ調整) ---
this.fsMainTitle = 32;
this.fsTitle = 30;
this.fwTitle = "bold";
this.fsWeek = 20;
this.fsDay = 21; // セルに合わせて少し小さく
this.fwDayNormal = "bold";
this.fwDayBold = "bold";
// CSS Variables
const style = getComputedStyle(document.documentElement);
this.colMain = style.getPropertyValue('--color-text-main').trim();
this.colSat = style.getPropertyValue('--color-text-sat').trim();
this.colSun = style.getPropertyValue('--color-text-sun').trim();
this.colWeek = style.getPropertyValue('--color-text-week').trim();
this.colHolidayBg = "rgba(230, 0, 18, 0.15)";
this.colSatBg = "rgba(0, 160, 233, 0.15)";
this.wMonthBlock = (this.wCell * 7);
// ブロック高さの内部マージンも極限まで削る
// Title(25) + Week(20) + Days
this.hMonthBlock = 25 + 20 + (this.hCell * 6);
}
build() {
const elSvg = document.createElementNS(this.svgNS, "svg");
const cntCols = 2;
const cntRows = 6;
const wTotal = (this.wMonthBlock * cntCols) + (this.gapMonthX * (cntCols - 1));
const hTotal = this.hTitleArea + (this.hMonthBlock * cntRows) + (this.gapMonthY * (cntRows - 1));
elSvg.setAttribute("viewBox", `0 0 ${wTotal} ${hTotal}`);
elSvg.setAttribute("width", wTotal*1.2);
elSvg.setAttribute("height", hTotal);
// Main Title
elSvg.appendChild(this.createSvgText(
`${this.year}年カレンダー`,
wTotal / 2, this.hTitleArea / 2,
this.fsMainTitle, this.colMain, this.fwTitle
));
for (let m = 1; m <= 12; m++) {
const idxGrid = m - 1;
const idxCol = Math.floor(idxGrid / cntRows);
const idxRow = idxGrid % cntRows;
const posX = idxCol * (this.wMonthBlock + this.gapMonthX);
const posY = this.hTitleArea + (idxRow * (this.hMonthBlock + this.gapMonthY));
elSvg.appendChild(this.createMonthGroup(m, posX, posY));
}
return elSvg;
}
createMonthGroup(month, offsetX, offsetY) {
const g = document.createElementNS(this.svgNS, "g");
g.setAttribute("transform", `translate(${offsetX}, ${offsetY})`);
// Month Title (位置調整)
g.appendChild(this.createSvgText(`${month}月`, this.wMonthBlock / 2, 12, this.fsTitle, this.colMain, this.fwTitle));
// Week Headers (位置調整)
const arrWeeks = ["日", "月", "火", "水", "木", "金", "土"];
const yWeek = 35;
arrWeeks.forEach((txt, i) => {
let color = this.colWeek;
if (i === 0) color = this.colSun;
else if (i === 6) color = this.colSat;
g.appendChild(this.createSvgText(txt, (i * this.wCell) + (this.wCell / 2), yWeek, this.fsWeek, color, "bold"));
});
// Days
const dateIter = new Date(this.year, month - 1, 1);
let idxDayOfWeek = dateIter.getDay();
let idxWeekRow = 0;
const yDateStart = 58; // 日付の開始位置も詰める
while (dateIter.getMonth() === month - 1) {
const day = dateIter.getDate();
const strDateKey = this.formatDateKey(this.year, month, day);
const x = (idxDayOfWeek * this.wCell) + (this.wCell / 2);
const y = yDateStart + (idxWeekRow * this.hCell);
const isHoliday = mapHolidays.hasOwnProperty(strDateKey);
const isSunday = (idxDayOfWeek === 0);
const isSaturday = (idxDayOfWeek === 6);
let color = this.colMain;
let fontWeight = this.fwDayNormal;
let circleColor = null;
if (isHoliday || isSunday) {
color = this.colSun;
fontWeight = this.fwDayBold;
circleColor = this.colHolidayBg;
} else if (isSaturday) {
color = this.colSat;
fontWeight = this.fwDayBold;
circleColor = this.colSatBg;
}
if (circleColor) {
const elCircle = document.createElementNS(this.svgNS, "circle");
elCircle.setAttribute("cx", x);
elCircle.setAttribute("cy", y);
// セルの高さに合わせて円の半径を自動調整
// Math.minを使って枠からはみ出さないようにする
elCircle.setAttribute("r", (Math.min(this.wCell, this.hCell) / 2*1.13) - 1);
elCircle.setAttribute("fill", circleColor);
g.appendChild(elCircle);
}
g.appendChild(this.createSvgText(day, x, y, this.fsDay, color, fontWeight));
dateIter.setDate(day + 1);
idxDayOfWeek++;
if (idxDayOfWeek > 6) { idxDayOfWeek = 0; idxWeekRow++; }
}
return g;
}
createSvgText(text, x, y, fontSize, fill, fontWeight) {
const el = document.createElementNS(this.svgNS, "text");
el.textContent = text;
el.setAttribute("x", x);
el.setAttribute("y", y);
el.setAttribute("text-anchor", "middle");
el.setAttribute("fill", fill);
el.setAttribute("font-size", fontSize);
el.setAttribute("font-weight", fontWeight);
el.setAttribute("dominant-baseline", "central");
return el;
}
formatDateKey(y, m, d) {
return `${y}-${m.toString().padStart(2,'0')}-${d.toString().padStart(2,'0')}`;
}
}
// ============================================================
// [Main]
// ============================================================
window.onload = () => {
const builder = new SvgCalendarBuilder(2026);
document.getElementById('svg-container').appendChild(builder.build());
};
function downloadAsSvg() {
const svgElement = document.querySelector("#svg-container svg");
const serializer = new XMLSerializer();
let source = serializer.serializeToString(svgElement);
if(!source.match(/^<svg[^>]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)){
source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
}
source = '<?xml version="1.0" standalone="no"?>\r\n' + source;
const url = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source);
const link = document.createElement("a");
link.download = "2026_Calendar_Density.svg";
link.href = url;
link.click();
}
function downloadAsPng() {
const svgElement = document.querySelector("#svg-container svg");
const serializer = new XMLSerializer();
const svgString = serializer.serializeToString(svgElement);
const scale = 2;
const width = parseInt(svgElement.getAttribute("width"));
const height = parseInt(svgElement.getAttribute("height"));
const canvas = document.createElement("canvas");
canvas.width = width * scale;
canvas.height = height * scale;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.scale(scale, scale);
const img = new Image();
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgString);
img.onload = () => {
ctx.drawImage(img, 0, 0);
const link = document.createElement("a");
link.download = "2026_Calendar_Density.png";
link.href = canvas.toDataURL("image/png");
link.click();
};
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment