Created
December 16, 2025 13:32
-
-
Save doshiraki/359b42d39b7153afa118d4ff843620ba to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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