Skip to the content

Tsunagi繋ぎ

Join each pair with a separate line and fill the board, through fixed levels or seeded puzzles.

Tsunagi means joining in Japanese. About the name

Contents

Reference · SVG board

TSUNAGI_STYLEconstant

THE STYLE a Tsunagi drawing wears: the colours of its board as custom properties, the two things that move (a flashing ring round a marble Check found unjoined, and the burst where an explosion took a line out), and the one rule that matters for a puzzle played with fingers: nothing in the drawing can be selected, dragged or double-tapped.

drawTsunagi only writes classes, data attributes and, for a board other than the plain paper, the custom properties below; this is what gives them a look. Every colour is a custom property on .tsunagi (--tsu-paper, --tsu-paper-deep, --tsu-frame, --tsu-grid, --tsu-ink, --tsu-coordinate, --tsu-shu, --tsu-good), so a page's own style needs to set only the ones it wants different. Paper follows the page's light or dark. With reduced motion asked for, nothing moves.

Import it

import { TSUNAGI_STYLE } from "@johnmorrisdotca/tsunagi/draw";

Definition

const TSUNAGI_STYLE: "\n.tsunagi {\n  --tsu-paper: #fbf8f1; --tsu-paper-deep: #fbf8f1; --tsu-frame: #a98954; --tsu-grid: #cfc6b2; --tsu-ink: #1f2320;\n  --tsu-coordinate: #5b3d1c; --tsu-shu: #d9381e; --tsu-good: #2f7a4f;\n  --tsu-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n  display: block; width: 100%; height: auto;\n  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; touch-action: none; -webkit-tap-highlight-color: transparent;\n  overflow: visible;\n}\n@media (prefers-color-scheme: dark) {\n  :root:not([data-theme=\"light\"]) .tsunagi[data-board=\"paper\"] { --tsu-paper: #262a27; --tsu-paper-deep: #262a27; --tsu-frame: #6b5632; --tsu-grid: #3f443f; --tsu-ink: #ece8dc; --tsu-coordinate: #e8d3b6; }\n}\n:root[data-theme=\"dark\"] .tsunagi[data-board=\"paper\"] { --tsu-paper: #262a27; --tsu-paper-deep: #262a27; --tsu-frame: #6b5632; --tsu-grid: #3f443f; --tsu-ink: #ece8dc; --tsu-coordinate: #e8d3b6; }\n.tsunagi * { user-select: none; -webkit-user-select: none; }\n.tsunagi .tsu-frame { fill: var(--tsu-frame); }\n.tsunagi .tsu-paper-stop-a { stop-color: var(--tsu-paper); }\n.tsunagi .tsu-paper-stop-b { stop-color: var(--tsu-paper-deep); }\n.tsunagi .tsu-solved { fill: var(--tsu-good); opacity: .14; pointer-events: none; }\n.tsunagi .tsu-grid { fill: none; stroke: var(--tsu-grid); stroke-width: 1px; vector-effect: non-scaling-stroke; }\n.tsunagi .tsu-hex-cell { fill: none; stroke: var(--tsu-grid); stroke-width: 1px; vector-effect: non-scaling-stroke; }\n.tsunagi .tsu-border { fill: none; stroke: var(--tsu-ink); stroke-width: 2.5px; vector-effect: non-scaling-stroke; opacity: .7; }\n.tsunagi .tsu-rim { fill: none; stroke: var(--tsu-ink); stroke-width: 2px; stroke-dasharray: 6 4; vector-effect: non-scaling-stroke; opacity: .7; }\n.tsunagi .tsu-blocked { fill: var(--tsu-ink); opacity: .55; }\n.tsunagi .tsu-wall { stroke: var(--tsu-ink); stroke-width: 14; stroke-linecap: round; }\n.tsunagi .tsu-deck { fill: var(--tsu-ink); opacity: .22; }\n.tsunagi .tsu-rail { stroke: var(--tsu-ink); stroke-width: 7; stroke-linecap: round; }\n.tsunagi .tsu-line polyline, .tsunagi .tsu-over-bridge { fill: none; stroke-width: 30; stroke-linecap: round; stroke-linejoin: round; }\n.tsunagi .tsu-num { font-family: var(--tsu-font); font-weight: 700; text-anchor: middle; font-variant-numeric: tabular-nums; pointer-events: none; }\n.tsunagi .tsu-coordinate { fill: var(--tsu-coordinate); font-family: var(--tsu-font); font-weight: 600; font-size: 24px; text-anchor: middle; pointer-events: none; }\n.tsunagi .tsu-glyph { font-family: var(--tsu-font); font-weight: 700; text-anchor: middle; pointer-events: none; }\n.tsunagi .tsu-portal-link { fill: none; stroke-width: 5; stroke-dasharray: 2 10; stroke-linecap: round; opacity: 0; pointer-events: none; transition: opacity .15s; }\n.tsunagi .tsu-portal:hover .tsu-portal-link, .tsunagi .tsu-portal[data-linked=\"true\"] .tsu-portal-link { opacity: .65; }\n.tsunagi .tsu-ghosts { opacity: .35; pointer-events: none; }\n.tsunagi .tsu-flag { fill: none; stroke-width: 8; transform-box: fill-box; transform-origin: center; animation: tsu-ping 1s cubic-bezier(0, 0, .2, 1) infinite; pointer-events: none; }\n.tsunagi .tsu-blast { fill: var(--tsu-shu); fill-opacity: .4; stroke: var(--tsu-shu); stroke-width: 8; transform-box: fill-box; transform-origin: center; animation: tsu-ping 1s cubic-bezier(0, 0, .2, 1) infinite; pointer-events: none; }\n@keyframes tsu-ping { 75%, 100% { transform: scale(1.7); opacity: 0; } }\n@media (prefers-reduced-motion: reduce) {\n  .tsunagi .tsu-flag, .tsunagi .tsu-blast { animation: none; }\n}\n"

Examples

import { TSUNAGI_STYLE } from "@johnmorrisdotca/tsunagi/draw";
console.log(TSUNAGI_STYLE);

.tsunagi {
  --tsu-paper: #fbf8f1; --tsu-paper-deep: #fbf8f1; --tsu-frame: #a98954; --tsu-grid: #cfc6b2; --tsu-ink: #1f2320;
  --tsu-coordinate: #5b3d1c; --tsu-shu: #d9381e; --tsu-good: #2f7a4f;
  --tsu-font: system-ui, -apple-system, "Segoe UI", sans-serif;
  display: block; width: 100%; height: auto;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; touch-action: none; -webkit-tap-highlight-color: transparent;
  overflow: visible;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tsunagi[data-board="paper"] { --tsu-paper: #262a27; --tsu-paper-deep: #262a27; --tsu-frame: #6b5632; --tsu-grid: #3f443f; --tsu-ink: #ece8dc; --tsu-coordinate: #e8d3b6; }
}
:root[data-theme="dark"] .tsunagi[data-board="paper"] { --tsu-paper: #262a27; --tsu-paper-deep: #262a27; --tsu-frame: #6b5632; --tsu-grid: #3f443f; --tsu-ink: #ece8dc; --tsu-coordinate: #e8d3b6; }
.tsunagi * { user-select: none; -webkit-user-select: none; }
.tsunagi .tsu-frame { fill: var(--tsu-frame); }
.tsunagi .tsu-paper-stop-a { stop-color: var(--tsu-paper); }
.tsunagi .tsu-paper-stop-b { stop-color: var(--tsu-paper-deep); }
.tsunagi .tsu-solved { fill: var(--tsu-good); opacity: .14; pointer-events: none; }
.tsunagi .tsu-grid { fill: none; stroke: var(--tsu-grid); stroke-width: 1px; vector-effect: non-scaling-stroke; }
.tsunagi .tsu-hex-cell { fill: none; stroke: var(--tsu-grid); stroke-width: 1px; vector-effect: non-scaling-stroke; }
.tsunagi .tsu-border { fill: none; stroke: var(--tsu-ink); stroke-width: 2.5px; vector-effect: non-scaling-stroke; opacity: .7; }
.tsunagi .tsu-rim { fill: none; stroke: var(--tsu-ink); stroke-width: 2px; stroke-dasharray: 6 4; vector-effect: non-scaling-stroke; opacity: .7; }
.tsunagi .tsu-blocked { fill: var(--tsu-ink); opacity: .55; }
.tsunagi .tsu-wall { stroke: var(--tsu-ink); stroke-width: 14; stroke-linecap: round; }
.tsunagi .tsu-deck { fill: var(--tsu-ink); opacity: .22; }
.tsunagi .tsu-rail { stroke: var(--tsu-ink); stroke-width: 7; stroke-linecap: round; }
.tsunagi .tsu-line polyline, .tsunagi .tsu-over-bridge { fill: none; stroke-width: 30; stroke-linecap: round; stroke-linejoin: round; }
.tsunagi .tsu-num { font-family: var(--tsu-font); font-weight: 700; text-anchor: middle; font-variant-numeric: tabular-nums; pointer-events: none; }
.tsunagi .tsu-coordinate { fill: var(--tsu-coordinate); font-family: var(--tsu-font); font-weight: 600; font-size: 24px; text-anchor: middle; pointer-events: none; }
.tsunagi .tsu-glyph { font-family: var(--tsu-font); font-weight: 700; text-anchor: middle; pointer-events: none; }
.tsunagi .tsu-portal-link { fill: none; stroke-width: 5; stroke-dasharray: 2 10; stroke-linecap: round; opacity: 0; pointer-events: none; transition: opacity .15s; }
.tsunagi .tsu-portal:hover .tsu-portal-link, .tsunagi .tsu-portal[data-linked="true"] .tsu-portal-link { opacity: .65; }
.tsunagi .tsu-ghosts { opacity: .35; pointer-events: none; }
.tsunagi .tsu-flag { fill: none; stroke-width: 8; transform-box: fill-box; transform-origin: center; animation: tsu-ping 1s cubic-bezier(0, 0, .2, 1) infinite; pointer-events: none; }
.tsunagi .tsu-blast { fill: var(--tsu-shu); fill-opacity: .4; stroke: var(--tsu-shu); stroke-width: 8; transform-box: fill-box; transform-origin: center; animation: tsu-ping 1s cubic-bezier(0, 0, .2, 1) infinite; pointer-events: none; }
@keyframes tsu-ping { 75%, 100% { transform: scale(1.7); opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .tsunagi .tsu-flag, .tsunagi .tsu-blast { animation: none; }
}

What it prints (run when this page was built)

View sourceEdit on GitHubMade from the source of @johnmorrisdotca/tsunagi 1.7.0.