Mini Scratch
button, [role="button"], select, summary, input:not([type="checkbox"], [type="radio"]) { min-height: 2rem; }
:root {
--bg: #e9f1f6;
--panel: #ffffff;
--panel-2: #f4f8fb;
--ink: #1d2a36;
--ink-soft: #5a7185;
--line: #d3dfe9;
--run: #1d9d63;
--run-dark: #157a4c;
--stop: #d64545;
--danger: #d64545;
--slot: #ffb35c;
--bezel: #2c3644;
--bezel-2: #1d242f;
--kb: #39434f;
--kb-key: #525f6e;
--kb-ink: #e8eef4;
--shadow: 0 1px 2px rgba(20, 40, 60, .08), 0 6px 18px rgba(20, 40, 60, .06);
--radius: 14px;
/* block colours */
--c-show: #2f9e63;
--c-hide: #5b7a99;
--c-spin: #f0a832;
--c-move: #3a86c8;
--c-jump: #00a3b4;
--c-say: #ef7aa5;
--c-size: #8e6cc8;
--c-colour: #c2187a;
--c-sound: #b07d3c;
--c-repeat: #e8862e;
--c-forever: #d95336;
--c-whenkey: #dd9d00;
--c-whenclick: #7cb518;
--goal-bg: #fff6dc;
--goal-line: #eec95f;
}
:root[data-theme="dark"] {
--bg: #0f161d;
--panel: #1a232e;
--panel-2: #212d3a;
--ink: #e8eff5;
--ink-soft: #9db2c3;
--line: #31424f;
--shadow: 0 1px 2px rgba(0,0,0,.4), 0 6px 18px rgba(0,0,0,.3);
--goal-bg: #2e2a12;
--goal-line: #6b5c1e;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "M PLUS Rounded 1c", "Hiragino Maru Gothic ProN", ui-rounded, system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 1560px; margin: 0 auto; padding: 12px clamp(10px, 1.6vw, 22px) 26px; }
/* ---------- Bilingual labels: small semi-transparent Japanese on top, English below ---------- */
.bi { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 1px; line-height: 1.12; vertical-align: middle; }
.bi-c { align-items: center; text-align: center; }
.bi-jp { font-size: .6em; font-weight: 700; opacity: .58; letter-spacing: .03em; white-space: nowrap; }
.bi-en { font-size: 1em; font-weight: 800; white-space: nowrap; }
.goalbar h1 .bi-jp { font-size: .52em; }
.char-btn .bi-en { font-size: .84rem; }
.char-btn .bi-jp { font-size: .62rem; }
.pi-txt { display: flex; flex-direction: column; gap: 2px; }
.loop-cap .bi-jp, .mini-block .bi-jp { font-size: .68em; }
/* Long explanations: English wraps, Japanese translation stays tiny above */
.bi-wrap { align-items: flex-start; max-width: 100%; }
.bi-wrap .bi-en { white-space: normal; line-height: 1.45; }
.bi-wrap .bi-jp { white-space: normal; line-height: 1.35; font-size: .68rem; }
.goalbar .goal .bi-jp { font-size: .78rem; }
.goalbar .goal .bi-en { font-size: 1.08rem; }
.exp { display: flex; flex-direction: column; gap: 1px; line-height: 1.4; }
.exp .exp-jp { font-size: .68rem; font-weight: 700; opacity: .58; letter-spacing: .02em; }
.exp .exp-en { font-size: .95em; font-weight: 700; }
.palette-tip .exp-en { font-weight: 500; color: var(--ink-soft); }
.modal p .exp-en { font-weight: 700; color: var(--ink-soft); }
.sub-bi .bi-jp { font-size: .62rem; }
.sub-bi .bi-en { font-size: .74rem; font-weight: 700; opacity: .92; }
/* ---------- Lesson navigation: one Lessons button + prev / next arrows ---------- */
.lesson-nav { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; position: relative; flex-wrap: wrap; }
.nav-arrow {
border: 2px solid var(--line); background: var(--panel); color: var(--ink);
border-radius: 50%; width: 48px; height: 48px; min-height: 48px; flex: 0 0 auto;
font-size: 1.25rem; font-weight: 800; cursor: pointer; line-height: 1;
display: inline-flex; align-items: center; justify-content: center;
box-shadow: var(--shadow);
}
.nav-arrow:hover:not(:disabled) { border-color: var(--ink-soft); background: var(--panel-2); }
.nav-arrow:disabled { opacity: .35; cursor: default; }
.lessons-wrap { position: relative; }
.lessons-btn {
border: 2px solid #2f9e63; background: #2f9e63; color: #fff;
border-radius: 999px; padding: 7px 20px 8px; cursor: pointer;
font: inherit; font-weight: 800; font-size: 1.05rem; min-height: 48px;
display: inline-flex; align-items: center; gap: 8px;
box-shadow: var(--shadow), inset 0 -3px 0 rgba(0,0,0,.18);
}
.lessons-btn:hover { background: #278553; border-color: #278553; }
.lessons-btn .chev { font-size: .85rem; opacity: .9; }
.lessons-btn .cur {
background: rgba(255,255,255,.24); border-radius: 999px; padding: 1px 10px;
font-size: .88rem; font-weight: 800;
}
.lesson-picker {
position: absolute; left: 0; top: calc(100% + 8px); z-index: 50;
background: var(--panel); border: 2px solid var(--line); border-radius: 14px;
box-shadow: 0 14px 40px rgba(0,0,0,.22); padding: 12px;
width: min(430px, calc(100vw - 32px));
}
.lesson-picker[hidden] { display: none; }
.lesson-picker .picker-hint { margin: 0 0 9px; font-size: .84rem; font-weight: 700; color: var(--ink-soft); }
.lesson-num-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 7px; }
.lesson-num {
border: 1.5px solid var(--line); background: var(--panel-2); color: var(--ink);
border-radius: 10px; padding: 8px 0; cursor: pointer;
font: inherit; font-weight: 800; font-size: 1.05rem; min-height: 44px;
display: inline-flex; align-items: center; justify-content: center;
}
.lesson-num:hover { border-color: #2f9e63; background: #e7f6ee; color: #14532d; }
:root[data-theme="dark"] .lesson-num:hover { background: #173527; color: #e8eff5; }
.lesson-num[aria-current="true"] { background: #2f9e63; border-color: #2f9e63; color: #fff; }
.lesson-nav-label { font-size: .92rem; font-weight: 700; color: var(--ink-soft); }
.goalbar {
display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px;
background: var(--goal-bg); border: 2px solid var(--goal-line); border-radius: var(--radius);
box-shadow: var(--shadow); padding: 12px 16px; margin-bottom: 14px;
}
.goalbar h1 { margin: 0; font-size: 1.25rem; font-weight: 800; white-space: nowrap; }
.goal-tag {
background: #f2a007; color: #fff; border-radius: 999px; padding: 5px 13px;
font-size: .88rem; font-weight: 800; white-space: nowrap;
box-shadow: inset 0 -3px 0 rgba(0,0,0,.18);
}
.goalbar .goal { margin: 0; font-size: 1.1rem; font-weight: 800; color: var(--ink); flex: 1 1 320px; line-height: 1.5; }
.newpiece {
background: #fff3d6; color: #8a5a00; border: 1.5px dashed #e6b93f;
border-radius: 999px; padding: 4px 12px; font-size: .85rem; font-weight: 800; white-space: nowrap;
}
:root[data-theme="dark"] .newpiece { background: #3a2f14; color: #ffd97a; }
.file-tools { display: flex; gap: 8px; margin-left: auto; }
.file-tools button {
border: 1.5px solid var(--line); background: var(--panel-2); color: var(--ink);
border-radius: 10px; padding: 6px 14px; font: inherit; font-weight: 800; font-size: .9rem;
cursor: pointer; min-height: 40px;
}
.file-tools button:hover { border-color: var(--ink-soft); }
/* ---------- Main layout: wide / landscape first ---------- */
.layout {
display: grid;
grid-template-columns: 232px minmax(0, 1fr) 424px;
gap: 14px;
align-items: start;
}
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.panel-head {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
padding: 10px 14px;
border-bottom: 1px solid var(--line);
background: var(--panel-2);
}
.panel-head h2 { margin: 0; font-size: .98rem; font-weight: 800; }
.panel-head .hint { font-size: .8rem; color: var(--ink-soft); font-weight: 700; }
/* ---------- Palette ---------- */
.palette-body { padding: 12px; display: flex; flex-direction: column; gap: 10px; }
.palette-item {
display: flex; align-items: center; gap: 9px;
padding: 10px 12px;
border-radius: 10px 10px 10px 4px;
color: #fff; font-weight: 800; font-size: 1rem; line-height: 1.15;
cursor: grab; user-select: none; -webkit-user-select: none;
touch-action: none; position: relative;
box-shadow: inset 0 -3px 0 rgba(0,0,0,.18);
}
.palette-item::before {
content: ""; position: absolute; top: -5px; left: 16px;
width: 26px; height: 10px; border-radius: 5px 5px 0 0; background: inherit;
}
.palette-item:active { cursor: grabbing; }
.palette-item small { display: block; font-weight: 700; font-size: .74rem; opacity: .88; }
.palette-item .grip { margin-left: auto; opacity: .7; font-size: .95rem; letter-spacing: 1px; }
.palette-item.hat { border-radius: 22px 22px 10px 4px; }
.palette-item.hat::before { display: none; }
.c-show { background: var(--c-show); } .c-hide { background: var(--c-hide); }
.c-spin { background: var(--c-spin); } .c-move { background: var(--c-move); }
.c-jump { background: var(--c-jump); } .c-say { background: var(--c-say); }
.c-size { background: var(--c-size); } .c-colour { background: var(--c-colour); }
.c-sound { background: var(--c-sound); } .c-repeat { background: var(--c-repeat); }
.c-forever { background: var(--c-forever); }
.c-whenkey { background: var(--c-whenkey); color: #3a2a00; }
.c-whenclick { background: var(--c-whenclick); }
.target-note {
border: 1.5px dashed var(--line); border-radius: 10px;
padding: 9px 11px; font-size: .86rem; font-weight: 700; color: var(--ink-soft); line-height: 1.4;
}
.target-note strong { color: var(--ink); }
.target-note button {
margin-top: 6px; border: 1px solid var(--line); background: var(--panel-2);
color: var(--ink); border-radius: 8px; padding: 4px 10px;
font: inherit; font-size: .8rem; font-weight: 700; cursor: pointer; min-height: 36px;
}
.palette-tip { font-size: .82rem; color: var(--ink-soft); font-weight: 500; line-height: 1.5; margin: 0; }
.show-more-btn {
border: 2px dashed var(--line); background: var(--panel-2); color: var(--ink);
border-radius: 10px; padding: 9px 10px; font: inherit; font-weight: 800; font-size: .92rem;
cursor: pointer; min-height: 46px; width: 100%;
}
.show-more-btn:hover { border-color: var(--ink-soft); }
.palette-section-label { font-size: .8rem; font-weight: 800; color: var(--ink-soft); }
/* ---------- Workspace ---------- */
.char-tabs { display: none; gap: 6px; }
.char-tabs.on { display: flex; }
.char-tabs button {
border: 1.5px solid var(--line); background: var(--panel); color: var(--ink-soft);
border-radius: 999px; padding: 4px 12px; font: inherit; font-size: .82rem; font-weight: 800;
cursor: pointer; min-height: 36px;
}
.char-tabs button[aria-selected="true"] { background: var(--ink); color: var(--panel); border-color: var(--ink); }
.ws-tools { display: flex; flex-wrap: wrap; gap: 6px; }
.ws-tools button {
border: 1px solid var(--line); background: var(--panel); color: var(--ink);
border-radius: 999px; padding: 4px 12px; font: inherit; font-size: .82rem; font-weight: 700;
cursor: pointer; min-height: 36px;
}
.ws-tools button.danger { color: var(--danger); }
.workspace {
padding: 14px 14px 20px;
min-height: 430px;
background: radial-gradient(circle, var(--line) 1.1px, transparent 1.2px) 0 0 / 22px 22px;
}
.empty-note {
border: 1.5px dashed var(--line); border-radius: 10px;
padding: 22px 16px; text-align: center; color: var(--ink-soft);
font-weight: 700; font-size: .95rem; background: var(--panel); line-height: 1.6;
}
.slot { height: 8px; border-radius: 99px; margin: 1px 2px; transition: background .08s, height .08s; }
.slot.active { background: var(--slot); height: 12px; box-shadow: 0 0 0 2px rgba(255,179,92,.35); }
.block { position: relative; user-select: none; -webkit-user-select: none; }
.block-head {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
color: #fff; font-weight: 800; font-size: 1rem;
padding: 9px 10px 11px 14px;
border-radius: 10px 10px 10px 4px;
box-shadow: inset 0 -3px 0 rgba(0,0,0,.18);
cursor: grab; touch-action: none; position: relative;
}
.block-head::before {
content: ""; position: absolute; top: -5px; left: 16px;
width: 26px; height: 10px; border-radius: 5px 5px 0 0; background: inherit;
}
.block-head:active { cursor: grabbing; }
.block.hat > .block-head { border-radius: 24px 24px 10px 4px; padding-top: 12px; }
.block.hat > .block-head::before { display: none; }
.b-whenkey > .block-head { color: #3a2a00; }
.block.container > .block-head { border-radius: 10px 10px 4px 4px; }
.block.hat.container > .block-head { border-radius: 24px 24px 4px 4px; }
.block.running > .block-head { outline: 3px solid #ffd23e; outline-offset: 1px; }
.block.selected > .block-head { outline: 3px solid var(--ink); outline-offset: 1px; }
.block-head .lbl { white-space: nowrap; }
.block-head input[type="text"] {
flex: 1 1 110px; min-width: 80px;
border: none; border-radius: 7px;
padding: 5px 9px; font: inherit; font-weight: 700; font-size: .95rem;
background: rgba(255,255,255,.94); color: #20303f; min-height: 36px;
}
.block-head input[type="number"] {
width: 62px; border: none; border-radius: 7px; text-align: center;
padding: 5px 4px; font: inherit; font-weight: 800; font-size: .95rem;
background: rgba(255,255,255,.94); color: #20303f; min-height: 36px;
}
.block-head select {
border: none; border-radius: 7px; padding: 5px 8px;
font: inherit; font-weight: 800; font-size: .92rem;
background: rgba(255,255,255,.94); color: #20303f; min-height: 36px; cursor: pointer;
}
.block-head input:focus, .block-head select:focus { outline: 3px solid rgba(255,210,63,.9); }
.dirbtn {
border: none; border-radius: 7px; width: 40px; height: 36px; min-height: 36px;
background: rgba(255,255,255,.94); color: #20303f; font-size: 1.05rem; font-weight: 800; cursor: pointer;
}
.del {
margin-left: auto; flex: 0 0 auto;
width: 36px; height: 36px; min-height: 36px;
border: none; border-radius: 8px; cursor: pointer;
background: rgba(0,0,0,.18); color: inherit; font-size: 1rem; font-weight: 800; line-height: 1;
}
.del:hover { background: rgba(0,0,0,.32); }
.loop-body {
border-left: 14px solid;
border-radius: 0 0 0 6px;
padding: 6px 4px 6px 10px;
min-height: 44px;
}
.b-repeat > .loop-body { border-left-color: var(--c-repeat); }
.b-forever > .loop-body { border-left-color: var(--c-forever); }
.b-whenkey > .loop-body { border-left-color: var(--c-whenkey); }
.b-whenclick > .loop-body { border-left-color: var(--c-whenclick); }
.loop-empty { color: var(--ink-soft); font-size: .85rem; font-weight: 700; padding: 6px 4px; }
.loop-cap {
color: #fff; font-weight: 800; font-size: .82rem;
padding: 4px 10px 6px 14px; border-radius: 4px 4px 10px 10px;
box-shadow: inset 0 -3px 0 rgba(0,0,0,.18);
width: fit-content; min-width: 120px;
}
.b-repeat > .loop-cap { background: var(--c-repeat); }
.b-forever > .loop-cap { background: var(--c-forever); }
.b-whenkey > .loop-cap { background: var(--c-whenkey); color: #3a2a00; }
.b-whenclick > .loop-cap { background: var(--c-whenclick); }
.drag-ghost { position: fixed; z-index: 60; pointer-events: none; opacity: .92; filter: drop-shadow(0 10px 18px rgba(0,0,0,.3)); }
.drag-src { opacity: .35; }
/* ---------- Stage column : monitor + keyboard ---------- */
.stage-col { display: flex; flex-direction: column; gap: 12px; }
.monitor {
background: var(--bezel);
border-radius: 20px;
padding: 14px 14px 10px;
box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,.14);
}
.screen {
position: relative;
border-radius: 10px;
overflow: hidden;
aspect-ratio: 4 / 3;
background: #aee3f9;
}
.screen svg.backdrop { position: absolute; inset: 0; width: 100%; height: 100%; }
.screen .live-dot {
position: absolute; top: 8px; right: 10px; z-index: 6;
background: rgba(20,40,30,.55); color: #b8ffd2;
font-size: .72rem; font-weight: 800; border-radius: 999px; padding: 3px 10px;
display: none; align-items: center; gap: 6px;
}
.screen .live-dot::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: #3ddc74; }
.screen.live .live-dot { display: inline-flex; }
.bezel-bar {
display: flex; align-items: center; justify-content: center; gap: 8px;
padding-top: 9px; color: #93a3b5; font-size: .72rem; font-weight: 700; letter-spacing: .14em;
}
.bezel-bar .cam { width: 8px; height: 8px; border-radius: 50%; background: #57687c; box-shadow: inset 0 0 2px #000; }
.stand { display: flex; flex-direction: column; align-items: center; }
.stand .neck { width: 74px; height: 20px; background: var(--bezel-2); clip-path: polygon(18% 0, 82% 0, 100% 100%, 0 100%); }
.stand .base { width: 190px; height: 12px; background: var(--bezel); border-radius: 6px 6px 10px 10px; }
.actor-layer { position: absolute; inset: 0; }
.sprite {
position: absolute; bottom: 15%; width: 27%;
transform-origin: 50% 85%;
transition: transform .45s ease, opacity .25s ease, filter .3s ease;
cursor: pointer;
}
.sprite svg { display: block; width: 100%; height: auto; filter: drop-shadow(0 3px 3px rgba(30,60,30,.25)); }
.sprite.gone { opacity: 0; pointer-events: none; }
.bubble {
position: absolute; z-index: 5;
background: #fff; color: #20303f;
border: 2.5px solid #20303f; border-radius: 14px;
padding: 5px 12px; font-weight: 800; font-size: clamp(.8rem, 1.4vw, 1rem);
max-width: 62%; line-height: 1.3;
transform: translate(-50%, -100%);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bubble::after {
content: ""; position: absolute; left: 50%; bottom: -9px; transform: translateX(-50%);
border: 5px solid transparent; border-top: 7px solid #20303f; border-bottom: none;
}
.rock { position: absolute; bottom: 13.5%; right: 16%; width: 17%; z-index: 2; }
.stage-controls { display: flex; gap: 10px; align-items: stretch; }
.run-btn {
flex: 1; border: none; cursor: pointer; border-radius: 12px;
background: var(--run); color: #fff;
font: inherit; font-weight: 800; font-size: 1.12rem;
padding: 10px 18px; min-height: 52px;
box-shadow: inset 0 -4px 0 rgba(0,0,0,.18);
}
.run-btn:hover { background: var(--run-dark); }
.stop-btn {
border: none; cursor: pointer; border-radius: 12px;
background: var(--stop); color: #fff;
font: inherit; font-weight: 800; font-size: 1rem;
padding: 10px 16px; min-height: 52px;
box-shadow: inset 0 -4px 0 rgba(0,0,0,.18);
}
.reset-btn {
border: 1.5px solid var(--line); background: var(--panel); color: var(--ink);
border-radius: 12px; font: inherit; font-weight: 800; font-size: .92rem;
padding: 10px 14px; cursor: pointer; min-height: 52px;
}
/* keyboard (decorative — tap it for the password) */
.keyboard {
background: var(--kb);
border-radius: 14px;
padding: 12px 12px 14px;
box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,.14);
cursor: pointer; border: none; width: 100%; text-align: left;
transition: transform .06s ease, box-shadow .15s ease;
}
.keyboard:hover { box-shadow: var(--shadow), 0 0 0 3px rgba(255,179,92,.55); }
.keyboard:active { transform: translateY(1px); }
.kb-row { display: flex; gap: 5px; margin-bottom: 5px; }
.kb-row:last-child { margin-bottom: 0; }
.key {
flex: 1 1 0; background: var(--kb-key); color: var(--kb-ink);
border-radius: 6px; text-align: center;
font-size: .68rem; font-weight: 700; line-height: 1; padding: 7px 0 6px;
box-shadow: inset 0 -2px 0 rgba(0,0,0,.35);
font-family: "IBM Plex Mono", monospace;
}
.key.wide { flex: 2.2 1 0; } .key.space { flex: 6 1 0; } .key.xl { flex: 1.6 1 0; }
.kb-note { margin: 6px 2px 0; font-size: .82rem; font-weight: 700; color: var(--ink-soft); text-align: center; }
/* character picker */
.picker { padding: 12px 14px 14px; }
.picker h3 { margin: 0 0 8px; font-size: .92rem; font-weight: 800; }
.picker h3 .who { color: var(--ink-soft); font-size: .8rem; font-weight: 700; }
.char-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
.char-btn {
border: 2px solid var(--line); background: var(--panel-2); border-radius: 12px;
padding: 6px 2px 4px; cursor: pointer; font: inherit;
display: flex; flex-direction: column; align-items: center; gap: 2px;
}
.char-btn svg { width: 82%; height: auto; display: block; }
.char-btn span { font-size: .74rem; font-weight: 800; color: var(--ink-soft); }
.char-btn[aria-pressed="true"] { border-color: var(--run); background: #e7f6ee; }
:root[data-theme="dark"] .char-btn[aria-pressed="true"] { background: #173527; }
.char-btn[aria-pressed="true"] span { color: var(--ink); }
.picker + .picker { border-top: 1px dashed var(--line); }
/* ---------- Modals ---------- */
.overlay {
position: fixed; inset: 0; z-index: 80;
background: rgba(15, 25, 35, .55);
display: none; align-items: center; justify-content: center;
padding: 18px;
}
.overlay.open { display: flex; }
.modal {
background: var(--panel); color: var(--ink);
border-radius: 18px; box-shadow: 0 18px 50px rgba(0,0,0,.35);
width: min(520px, 100%); max-height: 88vh; overflow: auto;
padding: 22px 22px 20px;
}
.modal h2 { margin: 0 0 6px; font-size: 1.3rem; font-weight: 800; }
.modal p { margin: 0 0 12px; font-weight: 700; color: var(--ink-soft); line-height: 1.55; font-size: .95rem; }
.modal .pw-row { display: flex; gap: 8px; }
.modal input[type="text"] {
flex: 1; border: 2px solid var(--line); border-radius: 10px;
padding: 9px 12px; font: inherit; font-weight: 700; font-size: 1.05rem;
background: var(--panel); color: var(--ink); min-height: 48px;
font-family: "IBM Plex Mono", monospace; letter-spacing: .04em;
}
.modal input[type="text"]:focus { outline: 3px solid rgba(255,179,92,.7); border-color: var(--slot); }
.modal .go {
border: none; background: var(--run); color: #fff; border-radius: 10px;
font: inherit; font-weight: 800; font-size: 1rem; padding: 9px 20px; cursor: pointer; min-height: 48px;
}
.modal .close {
border: 1.5px solid var(--line); background: var(--panel-2); color: var(--ink);
border-radius: 10px; font: inherit; font-weight: 800; font-size: .92rem;
padding: 8px 16px; cursor: pointer; min-height: 44px; margin-top: 14px;
}
.pw-msg { min-height: 1.5em; margin-top: 10px; font-weight: 800; font-size: .95rem; }
.pw-msg.err { color: var(--danger); }
.shake { animation: shake .3s ease; }
@keyframes shake { 0%,100% { transform: none; } 25% { transform: translateX(-6px); } 75% { transform: translateX(6px); } }
.answer-stage {
position: relative; border-radius: 12px; overflow: hidden;
aspect-ratio: 16 / 8; background: #aee3f9; margin-bottom: 14px;
border: 3px solid var(--bezel);
}
.answer-stage svg.backdrop { position: absolute; inset: 0; width: 100%; height: 100%; }
.answer-blocks { background: var(--panel-2); border: 1.5px dashed var(--line); border-radius: 12px; padding: 12px; }
.answer-blocks h3 { margin: 0 0 10px; font-size: .92rem; font-weight: 800; color: var(--ink-soft); }
.mini-block {
color: #fff; font-weight: 800; font-size: .92rem;
border-radius: 9px 9px 9px 4px; padding: 7px 12px 9px;
box-shadow: inset 0 -3px 0 rgba(0,0,0,.18); width: fit-content; max-width: 100%;
}
.mini-block.hat { border-radius: 20px 20px 9px 4px; }
.mini-kids { margin: 4px 0 4px 14px; display: flex; flex-direction: column; gap: 4px; }
.mini-stack { display: flex; flex-direction: column; gap: 4px; }
.mini-friend-label { font-weight: 800; font-size: .85rem; color: var(--ink-soft); margin: 10px 0 4px; }
.toast {
position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%) translateY(20px);
background: var(--ink); color: var(--panel); font-weight: 800; font-size: .95rem;
border-radius: 999px; padding: 10px 22px; z-index: 90; opacity: 0; pointer-events: none;
transition: opacity .2s ease, transform .2s ease; box-shadow: 0 10px 30px rgba(0,0,0,.3);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
@media (prefers-reduced-motion: reduce) { .shake { animation: none; } .sprite { transition: none; } }
/* ---------- Responsive: stack on narrow screens ---------- */
@media (max-width: 1180px) {
.layout { grid-template-columns: 210px minmax(0, 1fr); }
.stage-col { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 460px) minmax(0, 1fr); align-items: start; }
.stage-side { display: flex; flex-direction: column; gap: 12px; }
}
@media (max-width: 700px) {
.layout { grid-template-columns: 1fr; }
.stage-col { display: flex; }
.palette-body { flex-direction: row; flex-wrap: wrap; }
.palette-item { flex: 1 1 140px; }
.show-more-btn, .palette-section-label { flex: 1 1 100%; }
.target-note, .palette-tip { flex: 1 1 100%; }
.workspace { min-height: 300px; }
.goalbar h1 { font-size: 1.1rem; }
}
すうじを タップすると、その レッスンに いくよTap a number to go to that lesson
Lesson 1
もくひょう🎯 Goal
ピースPieces
タップか ドラッグTap or drag
この レッスンの ピースだけ みせているよ。「もっと みる」を おすと、ほかの ピースも でてきて つかえるよ。Only showing the pieces for this lesson. Tap “Show more” to see and use the other pieces too.
プログラムProgram
まだ ピースが ないよ。ひだりの ピースを タップして ついかしよう。No pieces yet.
Tap a piece on the left to add it.
あいことばを いれてねType the password
せんせいに あいことばを きいてね。あたると、レッスンの せいかいが みられるよ。Ask your teacher for the password.
Get it right and you can see the lesson's answer.
🎉 Correct!
せいかいの ピースは これだよHere are the answer pieces
"use strict";
/* ================= Characters ================= */
var CHARACTERS = [
{ id: "cat", name: "ねこ", nameEn: "Cat", voice: "にゃーん!", svg:
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' },
{ id: "dog", name: "いぬ", nameEn: "Dog", voice: "わん!", svg:
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' },
{ id: "rabbit", name: "うさぎ", nameEn: "Rabbit", voice: "ぴょん!", svg:
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' },
{ id: "bird", name: "とり", nameEn: "Bird", voice: "ぴよ!", svg:
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' },
{ id: "panda", name: "ぱんだ", nameEn: "Panda", voice: "やっほー!", svg:
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' },
{ id: "robot", name: "ロボット", nameEn: "Robot", voice: "ピピッ!", svg:
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' +
'' }
];
function charById(id) { for (var i = 0; i block array */
};
function progKey(lesson, slot) { return lesson + ":" + slot; }
function getProgram(lesson, slot) {
var k = progKey(lesson, slot);
if (!state.programs[k]) state.programs[k] = [];
return state.programs[k];
}
function program() { return getProgram(state.lesson, state.activeChar); }
function currentLesson() { return lessonByN(state.lesson); }
function friendActive() { return !!currentLesson().friend; }
/* ================= DOM refs ================= */
var lessonsBtn = document.getElementById("lessonsBtn");
var lessonsCur = document.getElementById("lessonsCur");
var lessonPicker = document.getElementById("lessonPicker");
var lessonNumGrid = document.getElementById("lessonNumGrid");
var prevLessonBtn = document.getElementById("prevLessonBtn");
var nextLessonBtn = document.getElementById("nextLessonBtn");
var lessonNavLabel = document.getElementById("lessonNavLabel");
var lessonTitleEl = document.getElementById("lessonTitle");
var lessonGoalEl = document.getElementById("lessonGoal");
var newPieceEl = document.getElementById("newPiece");
var paletteBody = document.getElementById("paletteBody");
var targetNote = document.getElementById("targetNote");
var listEl = document.getElementById("programList");
var emptyNote = document.getElementById("emptyNote");
var charTabsEl = document.getElementById("charTabs");
var charTab1 = document.getElementById("charTab1");
var charTab2 = document.getElementById("charTab2");
var wsTitle = document.getElementById("wsTitle");
var screenEl = document.getElementById("screen");
var rockEl = document.getElementById("rockEl");
var runBtn = document.getElementById("runBtn");
var toastEl = document.getElementById("toast");
/* ================= Bilingual labels: Japanese (small, semi-transparent) on top, English below ================= */
var TITLE_EN = ["Appear!", "Lots appear", "Spin", "Spin and stop", "Hide and seek", "Go for a walk", "Jump!", "In order", "Won't stop!", "Dance", "Talking", "Drive", "Jump button", "Big and small", "Rainbow", "A friend", "Talk together", "Sound", "Tap trick", "Jump the rock!", "Dance party", "A story", "Challenge", "Show day"];
var GOAL_EN = [
"Put the Show piece and press Start. Your character will appear!",
"Put Show inside Repeat and lots of characters will appear. Try changing the number.",
"Spin turns your character 45 degrees. Put it inside Repeat to turn many times — 8 spins make a full turn!",
"No new piece. Set Repeat to 4 — it turns 4 times, ends upside down, then stops all by itself.",
"Put Hide inside “When X key is pressed.” Press X to hide, press X again to come back.",
"Walk makes your character move forward. Tap the arrow to change direction. Can you go left too?",
"Jump makes your character hop. Put it inside Repeat to jump lots of times.",
"Pieces run in order from top to bottom. Line them up: Walk, then Jump, then Spin.",
"Forever keeps going and won't stop! Make it go back and forth, then press Stop.",
"Put Spin and Jump inside Forever to make a dance. Create your own dance!",
"Say makes a speech bubble appear. Make your character say your favorite words.",
"Change the key in “When key pressed” to the arrows. Move your character with the left and right keys.",
"Make Space jump. Walk with the arrow keys, jump with Space!",
"Size makes your character bigger or smaller.",
"Change color changes your character's color. Put it inside Forever for a rainbow dance!",
"Put two Show pieces and change the second one to Dog. Two characters will line up on the stage!",
"One character talks, then the friend answers. Use Show to change who is speaking, and make a little talk for two.",
"Sound plays your character's voice. Try it together with Jump or Spin.",
"Put a trick inside “When tapped.” Tap your character and your trick will happen!",
"There's a rock on the stage. Walk up to it and jump over it!",
"Spin + Color + Sound + Forever. Make a 10-second dance and start the party!",
"Use Say, Walk and Jump to make a story with 3 scenes: beginning, middle, and end.",
"Challenge cards: 1) Use 3 tricks 2) Hide, then jump 3) Make the biggest character. Pick your favorite!",
"Last lesson! Show your program to everyone. You're a programmer now!"
];
function expHTML(jp, en) {
return '
' + jp + '' + en + "";
}
var NEWPIECE_EN = {
"でる": "Show", "くりかえす": "Repeat", "まわる": "Spin",
"キーを おしたら + かくれる": "When key pressed + Hide",
"あるく": "Walk", "ジャンプ": "Jump", "ずっと くりかえす": "Forever",
"いう": "Say", "おおきさ": "Size", "いろを かえる": "Change color",
"おともだち": "A friend", "おとを ならす": "Sound", "タッチしたら": "When tapped"
};
function biEl(jp, en, center) {
var w = document.createElement("span");
w.className = "bi" + (center ? " bi-c" : "");
var j = document.createElement("span");
j.className = "bi-jp"; j.textContent = jp;
var e = document.createElement("span");
e.className = "bi-en"; e.textContent = en;
w.appendChild(j); w.appendChild(e);
return w;
}
function biHTML(jp, en, center) {
return '
' +
jp + '' + en + "";
}
var toastTimer = null;
function toast(en, jp) {
if (jp) {
toastEl.innerHTML = "";
var tw = document.createElement("span");
tw.className = "bi bi-c";
var tj = document.createElement("span"); tj.className = "bi-jp"; tj.textContent = jp;
var te = document.createElement("span"); te.className = "bi-en"; te.textContent = en;
tw.appendChild(tj); tw.appendChild(te);
toastEl.appendChild(tw);
} else {
toastEl.textContent = en;
}
toastEl.classList.add("show");
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2400);
}
/* ================= Tree helpers ================= */
function findBlock(id, arr) {
arr = arr || program();
for (var i = 0; i < arr.length; i++) {
if (arr[i].id === id) return arr[i];
if (CONTAINERS[arr[i].type]) {
var f = findBlock(id, arr[i].children);
if (f) return f;
}
}
return null;
}
function containerArray(key) {
if (key === "root") return program();
var b = findBlock(key);
return b ? b.children : program();
}
function locate(id, arr, key) {
arr = arr || program(); key = key || "root";
for (var i = 0; i < arr.length; i++) {
if (arr[i].id === id) return { arr: arr, index: i, key: key };
if (CONTAINERS[arr[i].type]) {
var f = locate(id, arr[i].children, arr[i].id);
if (f) return f;
}
}
return null;
}
var selectedContainerId = null;
/* ================= Workspace rendering ================= */
function slotEl(key, index) {
var s = document.createElement("div");
s.className = "slot";
s.dataset.key = key;
s.dataset.index = index;
return s;
}
function renderBlocks(arr, container, key) {
container.appendChild(slotEl(key, 0));
arr.forEach(function (b, i) {
container.appendChild(renderBlock(b));
container.appendChild(slotEl(key, i + 1));
});
}
var KEY_OPTIONS = [
{ v: "x", t: "X key" }, { v: "space", t: "Space" },
{ v: "left", t: "← key" }, { v: "right", t: "→ key" }
];
var KEY_JP = { x: "X キー", space: "スペース", left: "← キー", right: "→ キー" };
function keyLabel(v) {
for (var i = 0; i < KEY_OPTIONS.length; i++) if (KEY_OPTIONS[i].v === v) return KEY_OPTIONS[i].t;
return v;
}
function renderBlock(b) {
var def = BLOCK_DEFS[b.type];
var el = document.createElement("div");
el.className = "block b-" + b.type + (def.hat ? " hat" : "") + (def.container ? " container" : "");
el.dataset.id = b.id;
if (b.id === selectedContainerId) el.classList.add("selected");
var head = document.createElement("div");
head.className = "block-head c-" + b.type;
function lbl(jp, en) {
var s = biEl(jp, en);
s.classList.add("lbl");
return s;
}
if (b.type === "repeat") {
head.appendChild(lbl("くりかえす", "Repeat"));
var num = document.createElement("input");
num.type = "number"; num.min = "1"; num.max = "99"; num.value = b.times;
num.setAttribute("aria-label", "How many repeats");
num.addEventListener("input", function () {
var v = parseInt(num.value, 10);
b.times = isNaN(v) ? 1 : Math.max(1, Math.min(99, v));
});
num.addEventListener("change", function () { num.value = b.times; });
head.appendChild(num);
head.appendChild(lbl("かい", "times"));
} else if (b.type === "show") {
head.appendChild(lbl("でる", "Show"));
var csel = document.createElement("select");
csel.setAttribute("aria-label", "Which character to show");
CHARACTERS.forEach(function (ch) {
var op = document.createElement("option");
op.value = ch.id; op.textContent = ch.nameEn + " (" + ch.name + ")";
if (ch.id === (b.charId || "cat")) op.selected = true;
csel.appendChild(op);
});
csel.addEventListener("change", function () { b.charId = csel.value; });
head.appendChild(csel);
} else if (b.type === "forever") {
head.appendChild(lbl("ずっと くりかえす", "Forever"));
} else if (b.type === "whenkey") {
var sel = document.createElement("select");
sel.setAttribute("aria-label", "Which key");
KEY_OPTIONS.forEach(function (o) {
var op = document.createElement("option");
op.value = o.v; op.textContent = o.t;
if (o.v === b.key) op.selected = true;
sel.appendChild(op);
});
sel.addEventListener("change", function () { b.key = sel.value; });
head.appendChild(sel);
head.appendChild(lbl("を おしたら", "is pressed"));
} else if (b.type === "whenclick") {
head.appendChild(lbl("キャラを タッチしたら", "When tapped"));
} else if (b.type === "move") {
head.appendChild(lbl("あるく", "Walk"));
var steps = document.createElement("input");
steps.type = "number"; steps.min = "1"; steps.max = "10"; steps.value = b.steps;
steps.setAttribute("aria-label", "How many steps");
steps.addEventListener("input", function () {
var v = parseInt(steps.value, 10);
b.steps = isNaN(v) ? 1 : Math.max(1, Math.min(10, v));
});
steps.addEventListener("change", function () { steps.value = b.steps; });
head.appendChild(steps);
var dir = document.createElement("button");
dir.type = "button"; dir.className = "dirbtn";
dir.textContent = b.dir === 1 ? "→" : "←";
dir.setAttribute("aria-label", "Change direction");
dir.addEventListener("click", function (ev) {
ev.stopPropagation();
b.dir = b.dir === 1 ? -1 : 1;
render();
});
head.appendChild(dir);
} else if (b.type === "say") {
head.appendChild(lbl("いう", "Say"));
var inp = document.createElement("input");
inp.type = "text"; inp.value = b.text; inp.maxLength = 24;
inp.placeholder = charById(state.activeChar === 1 ? state.char1 : state.char2).voice;
inp.setAttribute("aria-label", "Words to say");
inp.addEventListener("input", function () { b.text = inp.value; });
head.appendChild(inp);
} else if (b.type === "size") {
head.appendChild(lbl("おおきさ", "Size"));
var ssel = document.createElement("select");
ssel.setAttribute("aria-label", "How to change size");
[["big", "Bigger"], ["small", "Smaller"]].forEach(function (pair) {
var op = document.createElement("option");
op.value = pair[0]; op.textContent = pair[1];
if (pair[0] === b.mode) op.selected = true;
ssel.appendChild(op);
});
ssel.addEventListener("change", function () { b.mode = ssel.value; });
head.appendChild(ssel);
} else {
head.appendChild(lbl(def.label, def.en));
}
var del = document.createElement("button");
del.type = "button"; del.className = "del"; del.textContent = "✕";
del.setAttribute("aria-label", "Delete this piece");
del.addEventListener("click", function (ev) {
ev.stopPropagation();
var loc = locate(b.id);
if (loc) loc.arr.splice(loc.index, 1);
if (selectedContainerId === b.id) selectedContainerId = null;
render();
});
head.appendChild(del);
el.appendChild(head);
head.addEventListener("pointerdown", function (ev) { startBlockDrag(ev, el, b); });
if (def.container) {
head.addEventListener("click", function () {
selectedContainerId = (selectedContainerId === b.id) ? null : b.id;
render();
});
}
if (def.container) {
var body = document.createElement("div");
body.className = "loop-body";
if (b.children.length === 0) {
body.appendChild(slotEl(b.id, 0));
var note = document.createElement("div");
note.className = "loop-empty";
note.appendChild(biEl("ここに ピースを いれよう", "Put pieces here"));
body.appendChild(note);
} else {
renderBlocks(b.children, body, b.id);
}
el.appendChild(body);
var cap = document.createElement("div");
cap.className = "loop-cap";
cap.appendChild(biEl(def.cap, def.capEn || "end"));
el.appendChild(cap);
}
return el;
}
function render() {
listEl.innerHTML = "";
renderBlocks(program(), listEl, "root");
emptyNote.hidden = program().length !== 0;
if (selectedContainerId && !findBlock(selectedContainerId)) selectedContainerId = null;
updateTargetNote();
}
function updateTargetNote() {
if (selectedContainerId) {
var b = findBlock(selectedContainerId);
targetNote.innerHTML = expHTML("あたらしい ピースは、えらんだ ピースの なかに はいるよ。", "New pieces will go
inside the “" + BLOCK_DEFS[b.type].en + "” you picked.");
var btn = document.createElement("button");
btn.type = "button"; btn.innerHTML = biHTML("いちばん したに いれる", "Put at the bottom", true);
btn.addEventListener("click", function () { selectedContainerId = null; render(); });
targetNote.appendChild(document.createElement("br"));
targetNote.appendChild(btn);
} else {
targetNote.innerHTML = expHTML("あたらしい ピースは プログラムの いちばん したに はいるよ。「くりかえす」などを タップすると、なかに いれられるよ。", "New pieces go at the
bottom of your program. Tap a piece like “Repeat” to put pieces inside it.");
}
}
/* ================= Palette ================= */
var paletteExpanded = false;
function makePaletteItem(type) {
var def = BLOCK_DEFS[type];
var item = document.createElement("div");
item.className = "palette-item c-" + type + (def.hat ? " hat" : "");
item.dataset.add = type;
item.setAttribute("role", "button");
item.setAttribute("tabindex", "0");
item.setAttribute("aria-label", "Add " + def.en + "(" + def.label + ")");
var txt = document.createElement("span");
txt.className = "pi-txt";
txt.appendChild(biEl(def.label, def.en));
var small = document.createElement("small");
small.className = "sub-bi";
if (def.subJp) {
var subBi = biEl(def.subJp, def.sub);
small.appendChild(subBi);
} else {
small.textContent = def.sub;
}
txt.appendChild(small);
var grip = document.createElement("span");
grip.className = "grip"; grip.setAttribute("aria-hidden", "true"); grip.textContent = "⠿";
item.appendChild(txt);
item.appendChild(grip);
item.addEventListener("click", function () { addBlock(type); });
item.addEventListener("keydown", function (ev) {
if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); addBlock(type); }
});
item.addEventListener("pointerdown", function (ev) { startPaletteDrag(ev, item); });
return item;
}
function renderPalette() {
paletteBody.querySelectorAll(".palette-item, .show-more-btn, .palette-section-label").forEach(function (el) { el.remove(); });
var lesson = currentLesson();
lesson.blocks.forEach(function (type) {
paletteBody.insertBefore(makePaletteItem(type), targetNote);
});
/* Pieces this lesson doesn't need yet stay hidden behind もっと みる — still fully usable */
var extras = Object.keys(BLOCK_DEFS).filter(function (type) { return lesson.blocks.indexOf(type) === -1; });
if (extras.length) {
var toggle = document.createElement("button");
toggle.type = "button";
toggle.className = "show-more-btn";
toggle.setAttribute("aria-expanded", paletteExpanded ? "true" : "false");
toggle.innerHTML = paletteExpanded ? biHTML("かくす", "▲ Hide", true) : biHTML("もっと みる(ほかの ピース)", "▼ Show more", true);
toggle.addEventListener("click", function () {
paletteExpanded = !paletteExpanded;
renderPalette();
});
paletteBody.insertBefore(toggle, targetNote);
if (paletteExpanded) {
var label = document.createElement("div");
label.className = "palette-section-label";
label.appendChild(biEl("そのほかの ピース — つかっても いいよ", "Other pieces — you can use these too"));
paletteBody.insertBefore(label, targetNote);
extras.forEach(function (type) {
paletteBody.insertBefore(makePaletteItem(type), targetNote);
});
}
}
}
function addBlock(type) {
var b = makeBlock(type);
if (type === "say") b.text = "";
var target = selectedContainerId ? containerArray(selectedContainerId) : program();
target.push(b);
if (CONTAINERS[type]) selectedContainerId = b.id;
render();
}
document.getElementById("clearProg").addEventListener("click", function () {
state.programs[progKey(state.lesson, state.activeChar)] = [];
selectedContainerId = null;
render();
});
/* ================= Drag & drop (pointer based, mouse + touch) ================= */
var drag = null;
var suppressClickUntil = 0;
function makeGhost(sourceEl, x, y) {
var g = sourceEl.cloneNode(true);
g.classList.add("drag-ghost");
var r = sourceEl.getBoundingClientRect();
g.style.width = Math.min(r.width, window.innerWidth - 24) + "px";
g.style.left = (x - 24) + "px";
g.style.top = (y - 18) + "px";
document.body.appendChild(g);
return g;
}
function moveGhost(x, y) {
if (!drag || !drag.ghost) return;
drag.ghost.style.left = (x - 24) + "px";
drag.ghost.style.top = (y - 18) + "px";
}
function nearestSlot(x, y) {
var best = null, bestD = Infinity;
document.querySelectorAll(".slot").forEach(function (s) {
if (drag && drag.blockId && s.closest && s.closest('.block[data-id="' + drag.blockId + '"]')) return;
var r = s.getBoundingClientRect();
if (x r.right + 30) return;
var d = Math.abs(y - (r.top + r.height / 2));
if (d < bestD) { bestD = d; best = s; }
});
return bestD < 90 ? best : null;
}
function highlightSlot(slot) {
document.querySelectorAll(".slot.active").forEach(function (s) { s.classList.remove("active"); });
if (slot) slot.classList.add("active");
}
function endDrag(commit) {
if (!drag) return;
var d = drag; drag = null;
if (d.ghost) d.ghost.remove();
if (d.srcEl) d.srcEl.classList.remove("drag-src");
highlightSlot(null);
if (commit && d.activeSlot) {
var key = d.activeSlot.dataset.key;
var index = parseInt(d.activeSlot.dataset.index, 10);
var target = containerArray(key);
if (d.isNew) {
target.splice(index, 0, d.newBlock);
if (CONTAINERS[d.newBlock.type]) selectedContainerId = d.newBlock.id;
} else {
var loc = locate(d.blockId);
if (loc) {
var blk = loc.arr.splice(loc.index, 1)[0];
if (loc.key === key && loc.index 7) {
drag.started = true;
drag.ghost = makeGhost(drag.srcEl, ev.clientX, ev.clientY);
if (!drag.isNew) drag.srcEl.classList.add("drag-src");
}
if (drag.started) {
ev.preventDefault();
moveGhost(ev.clientX, ev.clientY);
drag.activeSlot = nearestSlot(ev.clientX, ev.clientY);
highlightSlot(drag.activeSlot);
}
}
function trackUp(ev) {
if (!drag) return;
var wasDrag = drag.started;
if (wasDrag) suppressClickUntil = Date.now() + 300;
endDrag(true);
if (wasDrag) { ev.preventDefault(); ev.stopPropagation(); }
}
document.addEventListener("pointermove", trackMove, { passive: false });
document.addEventListener("pointerup", trackUp, true);
document.addEventListener("pointercancel", function () { endDrag(false); }, true);
document.addEventListener("click", function (ev) {
if (Date.now() 0) return;
drag = { isNew: false, blockId: b.id, srcEl: el, startX: ev.clientX, startY: ev.clientY, started: false, ghost: null, activeSlot: null };
}
function startPaletteDrag(ev, item) {
if (ev.button !== undefined && ev.button > 0) return;
drag = { isNew: true, newBlock: makeBlock(item.dataset.add), blockId: null, srcEl: item, startX: ev.clientX, startY: ev.clientY, started: false, ghost: null, activeSlot: null };
}
/* ================= Stage ================= */
/* Stage now holds any number of character instances.
Each Show piece creates a NEW instance with its own character,
so Show Cat + Show Dog (or Show inside Repeat) fills the stage.
The newest instance is the "active" one: Walk / Spin / Jump /
Say / Size / Colour / Sound / Hide act on it, until another
Show (or tapping a character) makes a different one active. */
var stageChars = [];
var activeIdx = -1;
function freshInstance(charId) {
return { charId: charId || "cat", gone: false, x: 0, y: 0, rot: 0, scale: 1, hue: 0, bubble: null, el: null, spinning: false };
}
function activeInstance() { return activeIdx >= 0 ? stageChars[activeIdx] : null; }
function ensureActive() {
if (!activeInstance()) {
stageChars.push(freshInstance("cat"));
activeIdx = stageChars.length - 1;
}
return activeInstance();
}
function renderStage() {
var layer = document.getElementById("layer1");
var layer2 = document.getElementById("layer2");
if (layer2) layer2.innerHTML = "";
var oldBubbles = layer.querySelectorAll(".bubble");
for (var bi = 0; bi next hat index */
function wait(ms, token) {
return new Promise(function (resolve) {
var started = Date.now();
(function check() {
if (token !== runToken) return resolve(false);
if (Date.now() - started >= ms) return resolve(true);
setTimeout(check, 30);
})();
});
}
function markRunning(id, on) {
var el = listEl.querySelector('.block[data-id="' + id + '"]');
if (el) el.classList.toggle("running", on);
}
function clampActor(a) {
var w = screenEl.clientWidth || 360;
var maxX = w * 0.34;
if (a.x > maxX) a.x = maxX;
if (a.x 2.6) a.scale = 2.6;
if (a.scale < 0.35) a.scale = 0.35;
}
async function execBlocks(arr, token) {
for (var i = 0; i < arr.length; i++) {
if (token !== runToken) return;
await execBlock(arr[i], token);
}
}
async function execBlock(b, token) {
if (token !== runToken) return;
var type = b.type;
if (type === "whenkey" || type === "whenclick") return; /* hats run on events */
markRunning(b.id, true);
try {
if (type === "show") {
if (stageChars.length 45deg in ~14 small increments
always shows the character turning, and it STAYS tilted by
45deg afterwards, so every spin is countable: 8 spins = one
full turn. */
beep(300, 700, 0.18);
var sa = ensureActive();
var spinFrom = sa.rot;
var spinTo = spinFrom + 45;
var spinStart = Date.now();
var spinDur = 420;
sa.spinning = true;
var spinDone = await new Promise(function (resolve) {
(function spinFrame() {
if (token !== runToken) return resolve(false);
var p = Math.min(1, (Date.now() - spinStart) / spinDur);
var e = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2; /* easeInOut */
sa.rot = spinFrom + (spinTo - spinFrom) * e;
renderStage();
if (p < 1) {
if (window.requestAnimationFrame) requestAnimationFrame(spinFrame);
else setTimeout(spinFrame, 16);
} else resolve(true);
})();
});
sa.rot = spinTo;
sa.spinning = false;
renderStage();
if (spinDone) await wait(80, token);
} else if (type === "move") {
var ma = ensureActive();
ma.x += b.dir * b.steps * 16;
clampActor(ma); renderStage();
await wait(420, token);
} else if (type === "jump") {
var ja = ensureActive();
ja.y = -52; renderStage();
beep(400, 900, 0.16);
await wait(270, token);
ja.y = 0; renderStage();
await wait(270, token);
} else if (type === "say") {
var ya = ensureActive();
var ch = charById(ya.charId);
ya.bubble = b.text && b.text.trim() ? b.text : ch.voice;
renderStage();
await wait(1150, token);
ya.bubble = null; renderStage();
} else if (type === "size") {
var za = ensureActive();
za.scale *= (b.mode === "big" ? 1.28 : 0.78);
clampActor(za); renderStage();
beep(b.mode === "big" ? 300 : 700, b.mode === "big" ? 520 : 380, 0.16);
await wait(340, token);
} else if (type === "colour") {
var ca = ensureActive();
ca.hue = (ca.hue + 45) % 360; renderStage();
beep(760, 1180, 0.14);
await wait(300, token);
} else if (type === "sound") {
var oa = ensureActive();
charSound(oa.charId);
await wait(430, token);
} else if (type === "repeat") {
var n = Math.max(1, Math.min(99, b.times || 1));
for (var k = 0; k = 0) activeIdx = ix;
var hats = allHats("whenclick");
hats.forEach(function (hat) { execBlocks(hat.children, runToken); });
}
document.addEventListener("keydown", function (ev) {
var tag = (ev.target && ev.target.tagName) || "";
if (tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA") return;
if (document.querySelector(".overlay.open")) return;
var key = null;
if (ev.key === "x" || ev.key === "X") key = "x";
else if (ev.key === " " || ev.key === "Spacebar") key = "space";
else if (ev.key === "ArrowLeft") key = "left";
else if (ev.key === "ArrowRight") key = "right";
if (key) { ev.preventDefault(); triggerKey(key); }
});
/* ================= Lesson navigation & switching ================= */
function closeLessonPicker() {
lessonPicker.hidden = true;
lessonsBtn.setAttribute("aria-expanded", "false");
}
function toggleLessonPicker() {
var willOpen = lessonPicker.hidden;
lessonPicker.hidden = !willOpen;
lessonsBtn.setAttribute("aria-expanded", willOpen ? "true" : "false");
}
function renderLessonTabs() {
lessonsCur.textContent = state.lesson + " / 24";
lessonNavLabel.innerHTML = "";
lessonNavLabel.appendChild(biEl("レッスン " + state.lesson + " — " + currentLesson().title, "Lesson " + state.lesson + " — " + TITLE_EN[state.lesson - 1]));
prevLessonBtn.disabled = state.lesson = 24;
lessonNumGrid.innerHTML = "";
LESSONS.forEach(function (L) {
var numBtn = document.createElement("button");
numBtn.type = "button";
numBtn.className = "lesson-num";
numBtn.textContent = L.n;
numBtn.setAttribute("role", "option");
numBtn.setAttribute("aria-current", L.n === state.lesson ? "true" : "false");
numBtn.setAttribute("aria-label", "Lesson " + L.n + " — " + TITLE_EN[L.n - 1]);
numBtn.addEventListener("click", function () { closeLessonPicker(); switchLesson(L.n); });
lessonNumGrid.appendChild(numBtn);
});
}
lessonsBtn.addEventListener("click", function (ev) { ev.stopPropagation(); toggleLessonPicker(); });
document.addEventListener("click", function (ev) {
if (!lessonPicker.hidden && !lessonPicker.contains(ev.target) && ev.target !== lessonsBtn) closeLessonPicker();
});
document.addEventListener("keydown", function (ev) { if (ev.key === "Escape") closeLessonPicker(); });
prevLessonBtn.addEventListener("click", function () { if (state.lesson > 1) switchLesson(state.lesson - 1); });
nextLessonBtn.addEventListener("click", function () { if (state.lesson < 24) switchLesson(state.lesson + 1); });
function switchLesson(n) {
n = Math.max(1, Math.min(24, n));
state.lesson = n;
state.activeChar = 1;
selectedContainerId = null;
paletteExpanded = false;
closeLessonPicker();
stopRun();
resetStage();
renderLessonTabs();
renderLessonHead();
renderPalette();
renderWorkspaceHead();
render();
}
function renderLessonHead() {
var L = currentLesson();
lessonTitleEl.innerHTML = "";
lessonTitleEl.appendChild(biEl("レッスン" + L.n + "「" + L.title + "」", "Lesson " + L.n + " — " + TITLE_EN[L.n - 1]));
lessonGoalEl.innerHTML = "";
var goalBi = biEl(L.goal, GOAL_EN[L.n - 1]);
goalBi.classList.add("bi-wrap");
lessonGoalEl.appendChild(goalBi);
if (L.newPiece) {
newPieceEl.hidden = false;
newPieceEl.innerHTML = biHTML("あたらしい ピース:" + L.newPiece, "✦ New piece: " + (NEWPIECE_EN[L.newPiece] || L.newPiece));
} else {
newPieceEl.hidden = true;
}
rockEl.hidden = L.prop !== "rock";
}
/* ================= Character tabs & pickers ================= */
function renderWorkspaceHead() {
/* One program per lesson now: characters are chosen on each
Show piece, so there are no character tabs or picker to show. */
charTabsEl.classList.remove("on");
wsTitle.style.display = "";
}
function renderPicker() { /* removed: character choice lives on the Show piece */ }
charTab1.addEventListener("click", function () { state.activeChar = 1; render(); });
charTab2.addEventListener("click", function () { state.activeChar = 1; render(); });
/* ================= Keyboard (decorative) ================= */
(function buildKeyboard() {
var rows = [
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"],
["A", "S", "D", "F", "G", "H", "J", "K", "L"],
["Z", "X", "C", "V", "B", "N", "M"]
];
var wrap = document.getElementById("kbRows");
rows.forEach(function (row) {
var r = document.createElement("div");
r.className = "kb-row";
row.forEach(function (k) {
var key = document.createElement("span");
key.className = "key";
key.textContent = k;
r.appendChild(key);
});
wrap.appendChild(r);
});
var last = document.createElement("div");
last.className = "kb-row";
[["←", "xl"], ["", "space"], ["→", "xl"]].forEach(function (pair) {
var key = document.createElement("span");
key.className = "key " + pair[1];
key.textContent = pair[0] || " ";
last.appendChild(key);
});
wrap.appendChild(last);
})();
/* ================= Password & answer ================= */
var pwOverlay = document.getElementById("pwOverlay");
var pwInput = document.getElementById("pwInput");
var pwMsg = document.getElementById("pwMsg");
var ansOverlay = document.getElementById("ansOverlay");
document.getElementById("keyboard").addEventListener("click", function () {
pwInput.value = "";
pwMsg.textContent = "";
pwMsg.className = "pw-msg";
pwOverlay.classList.add("open");
setTimeout(function () { pwInput.focus(); }, 60);
});
function closePw() { pwOverlay.classList.remove("open"); }
document.getElementById("pwClose").addEventListener("click", closePw);
pwOverlay.addEventListener("click", function (ev) { if (ev.target === pwOverlay) closePw(); });
function tryPassword() {
var guess = pwInput.value.trim().toLowerCase();
if (guess === pwFor(state.lesson)) {
closePw();
showAnswer();
} else {
pwMsg.innerHTML = ""; var _pm = document.createElement("span"); _pm.className = "exp"; _pm.innerHTML = '
ちがう みたい… もういちど かんがえてみようNot quite… think again and try once more'; pwMsg.appendChild(_pm);
pwMsg.className = "pw-msg err";
var modal = pwOverlay.querySelector(".modal");
modal.classList.remove("shake");
void modal.offsetWidth;
modal.classList.add("shake");
pwInput.select();
}
}
document.getElementById("pwGo").addEventListener("click", tryPassword);
pwInput.addEventListener("keydown", function (ev) { if (ev.key === "Enter") tryPassword(); });
document.getElementById("ansClose").addEventListener("click", function () { ansOverlay.classList.remove("open"); });
ansOverlay.addEventListener("click", function (ev) { if (ev.target === ansOverlay) ansOverlay.classList.remove("open"); });
function sceneHTML(scene, charId, friendId) {
var ch = charById(charId);
var html = '' +
'' +
'' +
'' +
'' +
'';
function sprite(opts) {
var style = "position:absolute;bottom:14%;width:24%;left:" + opts.left + "%;z-index:3;";
var tf = [];
if (opts.x) tf.push("translateX(" + opts.x + "px)");
if (opts.jump) tf.push("translateY(-30px)");
if (opts.spin) tf.push("rotate(45deg)");
if (tf.length) style += "transform:" + tf.join(" ") + ";transform-origin:50% 85%;";
if (opts.hue) style += "filter:hue-rotate(" + opts.hue + "deg);";
if (opts.big) style += "transform:" + tf.join(" ") + " scale(1.45);transform-origin:50% 100%;";
if (opts.peek) style += "opacity:.35;";
var s = '
' + charById(opts.char).svg + "
";
if (opts.bubble) {
s += '
' + opts.bubble + "
";
}
if (opts.key) {
s += '
' + opts.key + "
";
}
if (opts.tap) {
s += '
';
}
if (opts.note) {
s += '
♪
';
}
if (opts.arrows) {
s += '
← →
';
}
return s;
}
var left = scene.friend ? 10 : (scene.count > 1 ? 6 : 30);
if (scene.count > 1) {
for (var i = 0; i < scene.count; i++) {
html += sprite({ char: charId, left: 4 + i * 17, peek: scene.peek, key: i === 0 ? scene.key : null });
}
} else {
html += sprite({ char: charId, left: left, x: scene.x, jump: scene.jump, spin: scene.spin, hue: scene.hue, big: scene.big, peek: scene.peek, bubble: scene.bubble, key: scene.key, tap: scene.tap, note: scene.note, arrows: scene.arrows });
}
if (scene.friend) {
html += sprite({ char: friendId, left: 62, bubble: scene.friendBubble });
}
if (scene.rock) {
html += '';
}
return html;
}
function blockLabelPair(b) {
var ch = charById(state.char1);
switch (b.type) {
case "show": return { jp: "でる:" + charById(b.charId || "cat").name, en: "Show: " + charById(b.charId || "cat").nameEn };
case "hide": return { jp: "かくれる", en: "Hide" };
case "spin": return { jp: "まわる", en: "Spin" };
case "jump": return { jp: "ジャンプ", en: "Jump" };
case "colour": return { jp: "いろを かえる", en: "Change color" };
case "sound": return { jp: "おとを ならす", en: "Sound" };
case "repeat": return { jp: "くりかえす " + b.times + " かい", en: "Repeat " + b.times + " times" };
case "forever": return { jp: "ずっと くりかえす", en: "Forever" };
case "move": return { jp: "あるく " + b.steps + " " + (b.dir === 1 ? "→" : "←"), en: "Walk " + b.steps + " " + (b.dir === 1 ? "→" : "←") };
case "say": return { jp: "いう「" + (b.text === "VOICE" ? ch.voice : b.text) + "」", en: "Say \"" + (b.text === "VOICE" ? ch.voice : b.text) + "\"" };
case "size": return { jp: "おおきさ " + (b.mode === "big" ? "おおきく" : "ちいさく"), en: "Size: " + (b.mode === "big" ? "bigger" : "smaller") };
case "whenkey": return { jp: KEY_JP[b.key] + " を おしたら", en: keyLabel(b.key) + " pressed" };
case "whenclick": return { jp: "キャラを タッチしたら", en: "When tapped" };
default: return { jp: b.type, en: b.type };
}
}
function miniStack(blocks) {
var wrap = document.createElement("div");
wrap.className = "mini-stack";
blocks.forEach(function (b) {
var m = document.createElement("div");
m.className = "mini-block c-" + b.type + (BLOCK_DEFS[b.type].hat ? " hat" : "");
var pair = blockLabelPair(b);
m.appendChild(biEl(pair.jp, pair.en));
wrap.appendChild(m);
if (b.children && b.children.length) {
var kids = document.createElement("div");
kids.className = "mini-kids";
kids.appendChild(miniStack(b.children));
wrap.appendChild(kids);
var cap = document.createElement("div");
cap.className = "mini-block c-" + b.type;
cap.appendChild(biEl(BLOCK_DEFS[b.type].cap, BLOCK_DEFS[b.type].capEn || "end"));
wrap.appendChild(cap);
}
});
return wrap;
}
function showAnswer() {
var L = currentLesson();
document.getElementById("ansTitle").innerHTML = ""; document.getElementById("ansTitle").appendChild(biEl("せいかい! レッスン" + L.n + "の こたえ", "🎉 Correct! The answer for Lesson " + L.n));
document.getElementById("ansGoal").innerHTML = ""; var _ag = document.createElement("span"); _ag.className = "exp"; var _agj = document.createElement("span"); _agj.className = "exp-jp"; _agj.textContent = "もくひょう:" + L.goal; var _age = document.createElement("span"); _age.className = "exp-en"; _age.textContent = "Goal: " + GOAL_EN[L.n - 1]; _ag.appendChild(_agj); _ag.appendChild(_age); document.getElementById("ansGoal").appendChild(_ag);
var scene = {};
Object.keys(L.answer.scene).forEach(function (k) { scene[k] = L.answer.scene[k]; });
var ch = charById(state.char1);
if (scene.bubble === "VOICE") scene.bubble = ch.voice;
document.getElementById("ansStage").innerHTML = sceneHTML(scene, state.char1, state.char2);
var blocksWrap = document.getElementById("ansBlocks");
blocksWrap.innerHTML = "";
blocksWrap.appendChild(miniStack(L.answer.blocks.map(blockFromSpec)));
if (L.answer.friendBlocks) {
var fl = document.createElement("div");
fl.className = "mini-friend-label";
fl.innerHTML = ""; fl.appendChild(biEl(charById(state.char2).name + "の ピース:", charById(state.char2).nameEn + "'s pieces:"));
blocksWrap.appendChild(fl);
blocksWrap.appendChild(miniStack(L.answer.friendBlocks.map(blockFromSpec)));
}
ansOverlay.classList.add("open");
}
/* ================= Save / Load ================= */
document.getElementById("saveBtn").addEventListener("click", function () {
var data = {
app: "mini-scratch",
version: 3,
lesson: state.lesson,
char1: state.char1,
char2: state.char2,
programs: state.programs
};
var blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "mini-scratch-save-v3.json";
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function () { URL.revokeObjectURL(url); }, 2000);
toast("Saved your file!", "ファイルを ほぞんしたよ");
});
var loadFile = document.getElementById("loadFile");
document.getElementById("loadBtn").addEventListener("click", function () { loadFile.click(); });
loadFile.addEventListener("change", function () {
var file = loadFile.files && loadFile.files[0];
loadFile.value = "";
if (!file) return;
var reader = new FileReader();
reader.onload = function () {
try {
var data = JSON.parse(String(reader.result));
if (!data || data.app !== "mini-scratch" || typeof data.programs !== "object" || !data.programs) throw new Error("bad file");
/* Older saves (version 2 and earlier) stay safe: this new
version writes its own v3 file, and loading an old file
converts it — every old Show piece gets the character
that used to be picked in the separate picker window,
and a friend program (slot 2) is folded into slot 1 so
nothing is lost now that one program drives the stage. */
if (!data.version || data.version = 1 && n <= 24) ? n : 1;
state.activeChar = 1;
selectedContainerId = null;
paletteExpanded = false;
resetStage();
renderLessonTabs();
renderLessonHead();
renderPalette();
renderWorkspaceHead();
render();
/* keep ids unique for blocks added after loading */
Object.keys(state.programs).forEach(function (k) {
(function bump(arr) {
arr.forEach(function (b) {
var m = /^b(\d+)$/.exec(b.id || "");
if (m) uid = Math.max(uid, parseInt(m[1], 10));
if (b.children) bump(b.children);
});
})(state.programs[k]);
});
toast("Loaded!", "よみこんだよ!");
} catch (e) {
toast("That file could not be loaded", "その ファイルは よみこめなかったよ");
}
};
reader.readAsText(file);
});
/* ================= Theme switch (dark / light) ================= */
var themeBtn = document.getElementById("themeBtn");
var currentTheme = "light";
function renderThemeBtn() {
themeBtn.innerHTML = "";
if (currentTheme === "dark") {
themeBtn.appendChild(biEl("あかるくする", "☀️ Light", true));
themeBtn.setAttribute("aria-label", "Switch to light theme");
} else {
themeBtn.appendChild(biEl("くらくする", "🌙 Dark", true));
themeBtn.setAttribute("aria-label", "Switch to dark theme");
}
}
function applyTheme(t) {
currentTheme = t;
document.documentElement.setAttribute("data-theme", t);
document.documentElement.style.colorScheme = t;
renderThemeBtn();
}
themeBtn.addEventListener("click", function () {
applyTheme(currentTheme === "dark" ? "light" : "dark");
});
applyTheme(currentTheme);
/* ================= Init ================= */
renderLessonTabs();
renderLessonHead();
renderPalette();
renderWorkspaceHead();
render();
renderStage();