ippon-slides 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/assets/deck.js +346 -201
- package/assets/style.css +126 -2
- package/bin/ippon.js +1 -1
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +241 -0
- package/dist/highlight.d.ts +2 -0
- package/dist/highlight.js +105 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +227 -0
- package/dist/pdf.d.ts +11 -0
- package/dist/pdf.js +83 -0
- package/dist/render.d.ts +14 -0
- package/dist/render.js +52 -0
- package/package.json +15 -4
- package/src/cli.js +0 -236
- package/src/highlight.js +0 -113
- package/src/index.js +0 -3
- package/src/markdown.js +0 -232
- package/src/pdf.js +0 -89
- package/src/render.js +0 -64
package/README.md
CHANGED
|
@@ -91,6 +91,7 @@ const app = new Hono()
|
|
|
91
91
|
| `::bg=blue::` | 背景色。`blue` / `orange` / `purple` / `red` / `green` / `black` / `white`、または `#rrggbb` |
|
|
92
92
|
| `::size=1.5::` | 文字サイズの倍率 |
|
|
93
93
|
| `::right::` | ここから右カラム。先頭の見出しはカラムの上に出る |
|
|
94
|
+
| `::note::` | この行以降は発表者ノート。本番には出ず発表者ビュー(`p`)だけに出る |
|
|
94
95
|
| `-# 注釈` | 小さい注釈 |
|
|
95
96
|
| ` ```hcl ` | コード。`hcl` / `console` / `sh` / `json` / `js` / `ts` をハイライト |
|
|
96
97
|
| `!! ` | コードの行頭に付けるとその行を強調 |
|
|
@@ -107,10 +108,18 @@ const app = new Hono()
|
|
|
107
108
|
| `Home` / `End` | 最初 / 最後 |
|
|
108
109
|
| `f` | フルスクリーン |
|
|
109
110
|
| `o` / `Esc` | スライド一覧 |
|
|
111
|
+
| `p` | 発表者ビュー(別ウィンドウ) |
|
|
110
112
|
|
|
111
113
|
表紙から進むと右下でタイマーが動き、表紙に戻るとリセットされます。
|
|
112
114
|
URL の `#12` で 12 枚目から開けます。
|
|
113
115
|
|
|
116
|
+
### 発表者ビュー
|
|
117
|
+
|
|
118
|
+
`p` を押すと別ウィンドウで発表者ビューが開きます。現在のスライド・次のスライド・
|
|
119
|
+
`::note::` で書いたノート・経過タイマー・時計が手元に出て、本番側(プロジェクタに映す画面)は
|
|
120
|
+
通常表示のままです。どちらのウィンドウで `→` / `←` を押しても両画面が同期します。
|
|
121
|
+
発表者ビューは `p` / `Esc` で閉じます。
|
|
122
|
+
|
|
114
123
|
## 見た目を変える
|
|
115
124
|
|
|
116
125
|
`--css` で CSS 変数を上書きできます。
|
|
@@ -131,6 +140,19 @@ import { render, toPdf } from 'ippon-slides'
|
|
|
131
140
|
const { html } = render(markdown, { baseDir: '.', outDir: 'dist' })
|
|
132
141
|
```
|
|
133
142
|
|
|
143
|
+
型定義(`.d.ts`)も同梱しています。
|
|
144
|
+
|
|
145
|
+
## 開発
|
|
146
|
+
|
|
147
|
+
TypeScript で書いています。`src/` が Node 側、`src/client/deck.ts` がブラウザで動くナビゲーションです。
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
npm install
|
|
151
|
+
npm run build # dist/ と assets/deck.js を作る
|
|
152
|
+
npm run typecheck
|
|
153
|
+
npm test # ビルドしてからテスト(テストの実行には Node 22.18+ が必要)
|
|
154
|
+
```
|
|
155
|
+
|
|
134
156
|
## License
|
|
135
157
|
|
|
136
158
|
MIT
|
package/assets/deck.js
CHANGED
|
@@ -1,211 +1,356 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
// スライドのナビゲーション。ippon が HTML に埋め込む。
|
|
2
3
|
// → / Space / PageDown / クリック右側 次へ
|
|
3
4
|
// ← / PageUp / クリック左側 前へ
|
|
4
5
|
// Home / End 最初 / 最後
|
|
5
6
|
// f フルスクリーン
|
|
6
7
|
// o / Esc 一覧
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
var ticking = null
|
|
24
|
-
function fmt(ms) {
|
|
25
|
-
var s = Math.floor(ms / 1000)
|
|
26
|
-
var m = Math.floor(s / 60)
|
|
27
|
-
s = s % 60
|
|
28
|
-
return m + ':' + (s < 10 ? '0' : '') + s
|
|
29
|
-
}
|
|
30
|
-
function syncTimer() {
|
|
31
|
-
if (idx === 0) {
|
|
32
|
-
startedAt = null
|
|
33
|
-
clearInterval(ticking)
|
|
34
|
-
ticking = null
|
|
35
|
-
timerEl.textContent = ''
|
|
36
|
-
return
|
|
8
|
+
// p 発表者ビュー(別ウィンドウ)
|
|
9
|
+
(() => {
|
|
10
|
+
const slides = Array.from(document.querySelectorAll('#deck .slide'));
|
|
11
|
+
if (!slides.length)
|
|
12
|
+
return;
|
|
13
|
+
const $ = (id) => document.getElementById(id);
|
|
14
|
+
const div = (className) => {
|
|
15
|
+
const el = document.createElement('div');
|
|
16
|
+
el.className = className;
|
|
17
|
+
return el;
|
|
18
|
+
};
|
|
19
|
+
function fmt(ms) {
|
|
20
|
+
const total = Math.floor(ms / 1000);
|
|
21
|
+
const m = Math.floor(total / 60);
|
|
22
|
+
const s = total % 60;
|
|
23
|
+
return m + ':' + (s < 10 ? '0' : '') + s;
|
|
37
24
|
}
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}, 500)
|
|
25
|
+
// 別ウィンドウで開いたときは発表者ビューとして動く(本番のナビは動かさない)
|
|
26
|
+
if (window.name === 'ippon-presenter') {
|
|
27
|
+
presenterMode();
|
|
28
|
+
return;
|
|
43
29
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
window.addEventListener('hashchange', function () {
|
|
66
|
-
var n = parseInt(location.hash.slice(1), 10)
|
|
67
|
-
if (n) go(n - 1)
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
// ---------- 一覧 ----------
|
|
71
|
-
|
|
72
|
-
var overview = document.getElementById('overview')
|
|
73
|
-
var ovItems = null
|
|
74
|
-
var ovOpen = false
|
|
75
|
-
var THUMB = 240
|
|
76
|
-
|
|
77
|
-
function buildOverview() {
|
|
78
|
-
ovItems = slides.map(function (s, i) {
|
|
79
|
-
var item = document.createElement('div')
|
|
80
|
-
item.className = 'ov-item'
|
|
81
|
-
var frame = document.createElement('div')
|
|
82
|
-
frame.className = 'ov-frame'
|
|
83
|
-
var scale = document.createElement('div')
|
|
84
|
-
scale.className = 'ov-scale'
|
|
85
|
-
var clone = s.cloneNode(true)
|
|
86
|
-
clone.classList.remove('active')
|
|
87
|
-
scale.appendChild(clone)
|
|
88
|
-
frame.appendChild(scale)
|
|
89
|
-
var num = document.createElement('div')
|
|
90
|
-
num.className = 'ov-num'
|
|
91
|
-
num.textContent = i + 1
|
|
92
|
-
item.appendChild(frame)
|
|
93
|
-
item.appendChild(num)
|
|
94
|
-
item.addEventListener('click', function () {
|
|
95
|
-
idx = i
|
|
96
|
-
closeOverview()
|
|
97
|
-
render()
|
|
98
|
-
history.replaceState(null, '', '#' + (idx + 1))
|
|
99
|
-
})
|
|
100
|
-
overview.appendChild(item)
|
|
101
|
-
return { item: item, frame: frame, scale: scale }
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
function layoutOverview() {
|
|
105
|
-
var w = window.innerWidth
|
|
106
|
-
var h = window.innerHeight
|
|
107
|
-
var k = THUMB / w
|
|
108
|
-
ovItems.forEach(function (t) {
|
|
109
|
-
t.frame.style.width = THUMB + 'px'
|
|
110
|
-
t.frame.style.height = h * k + 'px'
|
|
111
|
-
t.scale.style.width = w + 'px'
|
|
112
|
-
t.scale.style.height = h + 'px'
|
|
113
|
-
t.scale.style.transform = 'scale(' + k + ')'
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
function openOverview() {
|
|
117
|
-
if (!ovItems) buildOverview()
|
|
118
|
-
layoutOverview()
|
|
119
|
-
ovItems.forEach(function (t, i) {
|
|
120
|
-
t.item.classList.toggle('current', i === idx)
|
|
121
|
-
})
|
|
122
|
-
ovOpen = true
|
|
123
|
-
document.body.classList.add('overview')
|
|
124
|
-
ovItems[idx].item.scrollIntoView({ block: 'center' })
|
|
125
|
-
}
|
|
126
|
-
function closeOverview() {
|
|
127
|
-
ovOpen = false
|
|
128
|
-
document.body.classList.remove('overview')
|
|
129
|
-
}
|
|
130
|
-
window.addEventListener('resize', function () {
|
|
131
|
-
if (ovOpen) layoutOverview()
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
// ---------- 入力 ----------
|
|
135
|
-
|
|
136
|
-
document.addEventListener('keydown', function (e) {
|
|
137
|
-
if (e.metaKey || e.ctrlKey || e.altKey) return
|
|
138
|
-
if (ovOpen) {
|
|
139
|
-
if (e.key === 'Escape' || e.key === 'o') closeOverview()
|
|
140
|
-
return
|
|
30
|
+
let idx = 0;
|
|
31
|
+
const progress = $('progress');
|
|
32
|
+
const counter = $('counter');
|
|
33
|
+
const timerEl = $('timer');
|
|
34
|
+
const clamp = (n) => Math.max(0, Math.min(slides.length - 1, n));
|
|
35
|
+
// ---------- タイマー(表紙から進んだら開始、表紙に戻ったらリセット) ----------
|
|
36
|
+
let startedAt = null;
|
|
37
|
+
let ticking;
|
|
38
|
+
function syncTimer() {
|
|
39
|
+
if (idx === 0) {
|
|
40
|
+
startedAt = null;
|
|
41
|
+
clearInterval(ticking);
|
|
42
|
+
ticking = undefined;
|
|
43
|
+
timerEl.textContent = '';
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const t0 = (startedAt ?? (startedAt = Date.now()));
|
|
47
|
+
ticking ?? (ticking = window.setInterval(() => {
|
|
48
|
+
timerEl.textContent = fmt(Date.now() - t0);
|
|
49
|
+
}, 500));
|
|
50
|
+
timerEl.textContent = fmt(Date.now() - t0);
|
|
141
51
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
case 'PageUp':
|
|
151
|
-
e.preventDefault()
|
|
152
|
-
go(idx - 1)
|
|
153
|
-
break
|
|
154
|
-
case 'Home':
|
|
155
|
-
go(0)
|
|
156
|
-
break
|
|
157
|
-
case 'End':
|
|
158
|
-
go(slides.length - 1)
|
|
159
|
-
break
|
|
160
|
-
case 'f':
|
|
161
|
-
if (document.fullscreenElement) document.exitFullscreen()
|
|
162
|
-
else document.documentElement.requestFullscreen()
|
|
163
|
-
break
|
|
164
|
-
case 'o':
|
|
165
|
-
openOverview()
|
|
166
|
-
break
|
|
167
|
-
case 'Escape':
|
|
168
|
-
if (!document.fullscreenElement) openOverview()
|
|
169
|
-
break
|
|
52
|
+
// ---------- 描画 ----------
|
|
53
|
+
function render() {
|
|
54
|
+
slides.forEach((s, i) => s.classList.toggle('active', i === idx));
|
|
55
|
+
slides[idx].scrollTop = 0;
|
|
56
|
+
counter.textContent = idx + 1 + ' / ' + slides.length;
|
|
57
|
+
progress.style.width = ((idx + 1) / slides.length) * 100 + '%';
|
|
58
|
+
syncTimer();
|
|
59
|
+
broadcast();
|
|
170
60
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
61
|
+
function go(n) {
|
|
62
|
+
n = clamp(n);
|
|
63
|
+
if (n === idx)
|
|
64
|
+
return;
|
|
65
|
+
idx = n;
|
|
66
|
+
render();
|
|
67
|
+
history.replaceState(null, '', '#' + (idx + 1));
|
|
68
|
+
}
|
|
69
|
+
window.addEventListener('hashchange', () => {
|
|
70
|
+
const n = parseInt(location.hash.slice(1), 10);
|
|
71
|
+
if (n)
|
|
72
|
+
go(n - 1);
|
|
73
|
+
});
|
|
74
|
+
// ---------- 発表者ビュー(別ウィンドウ)との同期 ----------
|
|
75
|
+
let presenterWin = null;
|
|
76
|
+
function openPresenter() {
|
|
77
|
+
if (presenterWin && !presenterWin.closed) {
|
|
78
|
+
presenterWin.focus();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
presenterWin = window.open(location.href, 'ippon-presenter', 'width=1024,height=700');
|
|
82
|
+
}
|
|
83
|
+
function broadcast() {
|
|
84
|
+
if (presenterWin && !presenterWin.closed)
|
|
85
|
+
presenterWin.postMessage({ ippon: 'state', idx, total: slides.length, startedAt }, '*');
|
|
86
|
+
}
|
|
87
|
+
window.addEventListener('message', (e) => {
|
|
88
|
+
const m = e.data;
|
|
89
|
+
if (!m || typeof m !== 'object')
|
|
90
|
+
return;
|
|
91
|
+
if (m.ippon === 'hello')
|
|
92
|
+
broadcast();
|
|
93
|
+
else if (m.ippon === 'go' && typeof m.n === 'number')
|
|
94
|
+
go(m.n);
|
|
95
|
+
});
|
|
96
|
+
const overview = $('overview');
|
|
97
|
+
let ovItems = null;
|
|
98
|
+
let ovOpen = false;
|
|
99
|
+
const THUMB = 240;
|
|
100
|
+
function buildOverview() {
|
|
101
|
+
return slides.map((s, i) => {
|
|
102
|
+
const item = div('ov-item');
|
|
103
|
+
const frame = div('ov-frame');
|
|
104
|
+
const scale = div('ov-scale');
|
|
105
|
+
const clone = s.cloneNode(true);
|
|
106
|
+
clone.classList.remove('active');
|
|
107
|
+
scale.appendChild(clone);
|
|
108
|
+
frame.appendChild(scale);
|
|
109
|
+
const num = div('ov-num');
|
|
110
|
+
num.textContent = String(i + 1);
|
|
111
|
+
item.appendChild(frame);
|
|
112
|
+
item.appendChild(num);
|
|
113
|
+
item.addEventListener('click', () => {
|
|
114
|
+
idx = i;
|
|
115
|
+
closeOverview();
|
|
116
|
+
render();
|
|
117
|
+
history.replaceState(null, '', '#' + (idx + 1));
|
|
118
|
+
});
|
|
119
|
+
overview.appendChild(item);
|
|
120
|
+
return { item, frame, scale };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function layoutOverview(items) {
|
|
124
|
+
const w = window.innerWidth;
|
|
125
|
+
const h = window.innerHeight;
|
|
126
|
+
const k = THUMB / w;
|
|
127
|
+
for (const t of items) {
|
|
128
|
+
t.frame.style.width = THUMB + 'px';
|
|
129
|
+
t.frame.style.height = h * k + 'px';
|
|
130
|
+
t.scale.style.width = w + 'px';
|
|
131
|
+
t.scale.style.height = h + 'px';
|
|
132
|
+
t.scale.style.transform = 'scale(' + k + ')';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function openOverview() {
|
|
136
|
+
const items = (ovItems ?? (ovItems = buildOverview()));
|
|
137
|
+
layoutOverview(items);
|
|
138
|
+
items.forEach((t, i) => t.item.classList.toggle('current', i === idx));
|
|
139
|
+
ovOpen = true;
|
|
140
|
+
document.body.classList.add('overview');
|
|
141
|
+
items[idx].item.scrollIntoView({ block: 'center' });
|
|
142
|
+
}
|
|
143
|
+
function closeOverview() {
|
|
144
|
+
ovOpen = false;
|
|
145
|
+
document.body.classList.remove('overview');
|
|
146
|
+
}
|
|
147
|
+
window.addEventListener('resize', () => {
|
|
148
|
+
if (ovOpen && ovItems)
|
|
149
|
+
layoutOverview(ovItems);
|
|
150
|
+
});
|
|
151
|
+
// ---------- 入力 ----------
|
|
152
|
+
document.addEventListener('keydown', (e) => {
|
|
153
|
+
if (e.metaKey || e.ctrlKey || e.altKey)
|
|
154
|
+
return;
|
|
155
|
+
if (ovOpen) {
|
|
156
|
+
if (e.key === 'Escape' || e.key === 'o')
|
|
157
|
+
closeOverview();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
switch (e.key) {
|
|
161
|
+
case 'ArrowRight':
|
|
162
|
+
case 'PageDown':
|
|
163
|
+
case ' ':
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
go(idx + 1);
|
|
166
|
+
break;
|
|
167
|
+
case 'ArrowLeft':
|
|
168
|
+
case 'PageUp':
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
go(idx - 1);
|
|
171
|
+
break;
|
|
172
|
+
case 'Home':
|
|
173
|
+
go(0);
|
|
174
|
+
break;
|
|
175
|
+
case 'End':
|
|
176
|
+
go(slides.length - 1);
|
|
177
|
+
break;
|
|
178
|
+
case 'f':
|
|
179
|
+
if (document.fullscreenElement)
|
|
180
|
+
document.exitFullscreen();
|
|
181
|
+
else
|
|
182
|
+
document.documentElement.requestFullscreen();
|
|
183
|
+
break;
|
|
184
|
+
case 'o':
|
|
185
|
+
openOverview();
|
|
186
|
+
break;
|
|
187
|
+
case 'p':
|
|
188
|
+
openPresenter();
|
|
189
|
+
break;
|
|
190
|
+
case 'Escape':
|
|
191
|
+
if (!document.fullscreenElement)
|
|
192
|
+
openOverview();
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
// 画面の右 3 割で次へ、左 3 割で前へ(リンクと文字選択は邪魔しない)
|
|
197
|
+
const deck = $('deck');
|
|
198
|
+
deck.addEventListener('click', (e) => {
|
|
199
|
+
if (e.target instanceof Element && e.target.closest('a, button'))
|
|
200
|
+
return;
|
|
201
|
+
if (String(window.getSelection() || ''))
|
|
202
|
+
return;
|
|
203
|
+
const x = e.clientX / window.innerWidth;
|
|
204
|
+
if (x > 0.7)
|
|
205
|
+
go(idx + 1);
|
|
206
|
+
else if (x < 0.3)
|
|
207
|
+
go(idx - 1);
|
|
208
|
+
});
|
|
209
|
+
// 横スワイプ
|
|
210
|
+
let touch = null;
|
|
211
|
+
deck.addEventListener('touchstart', (e) => {
|
|
212
|
+
const t = e.touches[0];
|
|
213
|
+
touch = e.touches.length === 1 && t ? { x: t.clientX, y: t.clientY } : null;
|
|
214
|
+
}, { passive: true });
|
|
215
|
+
deck.addEventListener('touchend', (e) => {
|
|
216
|
+
const t = e.changedTouches[0];
|
|
217
|
+
if (!touch || !t)
|
|
218
|
+
return;
|
|
219
|
+
const dx = t.clientX - touch.x;
|
|
220
|
+
const dy = t.clientY - touch.y;
|
|
221
|
+
touch = null;
|
|
222
|
+
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.5)
|
|
223
|
+
go(dx < 0 ? idx + 1 : idx - 1);
|
|
224
|
+
}, { passive: true });
|
|
225
|
+
const start = parseInt(location.hash.slice(1), 10);
|
|
226
|
+
if (start)
|
|
227
|
+
idx = clamp(start - 1);
|
|
228
|
+
render();
|
|
229
|
+
// ---------- 発表者ビュー本体(別ウィンドウで動く) ----------
|
|
230
|
+
function presenterMode() {
|
|
231
|
+
document.body.classList.add('presenter');
|
|
232
|
+
const root = $('presenter');
|
|
233
|
+
const left = div('pv-left');
|
|
234
|
+
const right = div('pv-right');
|
|
235
|
+
root.appendChild(left);
|
|
236
|
+
root.appendChild(right);
|
|
237
|
+
const makeSlot = (labelText) => {
|
|
238
|
+
const slot = div('pv-slot');
|
|
239
|
+
const label = div('pv-label');
|
|
240
|
+
label.textContent = labelText;
|
|
241
|
+
const frame = div('pv-frame');
|
|
242
|
+
const scale = div('pv-scale');
|
|
243
|
+
frame.appendChild(scale);
|
|
244
|
+
slot.appendChild(label);
|
|
245
|
+
slot.appendChild(frame);
|
|
246
|
+
left.appendChild(slot);
|
|
247
|
+
return { label, frame, scale };
|
|
248
|
+
};
|
|
249
|
+
const cur = makeSlot('現在');
|
|
250
|
+
const nxt = makeSlot('次へ');
|
|
251
|
+
const clock = div('pv-clock');
|
|
252
|
+
const elapsedEl = div('pv-elapsed');
|
|
253
|
+
const wallEl = div('pv-wall');
|
|
254
|
+
clock.appendChild(elapsedEl);
|
|
255
|
+
clock.appendChild(wallEl);
|
|
256
|
+
const controls = div('pv-controls');
|
|
257
|
+
const prevBtn = document.createElement('button');
|
|
258
|
+
prevBtn.textContent = '◀';
|
|
259
|
+
const nextBtn = document.createElement('button');
|
|
260
|
+
nextBtn.textContent = '▶';
|
|
261
|
+
controls.appendChild(prevBtn);
|
|
262
|
+
controls.appendChild(nextBtn);
|
|
263
|
+
const notesEl = div('pv-notes');
|
|
264
|
+
right.appendChild(clock);
|
|
265
|
+
right.appendChild(controls);
|
|
266
|
+
right.appendChild(notesEl);
|
|
267
|
+
let curIdx = 0;
|
|
268
|
+
let total = slides.length;
|
|
269
|
+
let startedAt = null;
|
|
270
|
+
function fitPreview(frame, scale) {
|
|
271
|
+
const w = window.innerWidth;
|
|
272
|
+
const h = window.innerHeight;
|
|
273
|
+
const fw = frame.clientWidth;
|
|
274
|
+
const k = fw / w;
|
|
275
|
+
frame.style.height = fw * (h / w) + 'px';
|
|
276
|
+
scale.style.width = w + 'px';
|
|
277
|
+
scale.style.height = h + 'px';
|
|
278
|
+
scale.style.transform = 'scale(' + k + ')';
|
|
279
|
+
}
|
|
280
|
+
function setPreview(frame, scale, i) {
|
|
281
|
+
scale.textContent = '';
|
|
282
|
+
const s = slides[i];
|
|
283
|
+
if (s)
|
|
284
|
+
scale.appendChild(s.cloneNode(true));
|
|
285
|
+
fitPreview(frame, scale);
|
|
286
|
+
}
|
|
287
|
+
function draw() {
|
|
288
|
+
cur.label.textContent = '現在 ' + (curIdx + 1) + ' / ' + total;
|
|
289
|
+
setPreview(cur.frame, cur.scale, curIdx);
|
|
290
|
+
const hasNext = curIdx + 1 < total;
|
|
291
|
+
nxt.label.textContent = hasNext ? '次へ ' + (curIdx + 2) + ' / ' + total : 'おわり';
|
|
292
|
+
setPreview(nxt.frame, nxt.scale, hasNext ? curIdx + 1 : -1);
|
|
293
|
+
const note = slides[curIdx]?.querySelector('.notes');
|
|
294
|
+
notesEl.innerHTML = note ? note.innerHTML : '<p class="pv-empty">(ノートなし)</p>';
|
|
295
|
+
}
|
|
296
|
+
function tick() {
|
|
297
|
+
elapsedEl.textContent = startedAt == null ? '0:00' : fmt(Date.now() - startedAt);
|
|
298
|
+
wallEl.textContent = new Date().toLocaleTimeString();
|
|
299
|
+
}
|
|
300
|
+
const send = (n) => {
|
|
301
|
+
if (window.opener)
|
|
302
|
+
window.opener.postMessage({ ippon: 'go', n }, '*');
|
|
303
|
+
};
|
|
304
|
+
prevBtn.addEventListener('click', () => send(curIdx - 1));
|
|
305
|
+
nextBtn.addEventListener('click', () => send(curIdx + 1));
|
|
306
|
+
window.addEventListener('message', (e) => {
|
|
307
|
+
const m = e.data;
|
|
308
|
+
if (!m || m.ippon !== 'state')
|
|
309
|
+
return;
|
|
310
|
+
if (typeof m.idx === 'number')
|
|
311
|
+
curIdx = m.idx;
|
|
312
|
+
if (typeof m.total === 'number')
|
|
313
|
+
total = m.total;
|
|
314
|
+
startedAt = m.startedAt ?? null;
|
|
315
|
+
draw();
|
|
316
|
+
tick();
|
|
317
|
+
});
|
|
318
|
+
document.addEventListener('keydown', (e) => {
|
|
319
|
+
if (e.metaKey || e.ctrlKey || e.altKey)
|
|
320
|
+
return;
|
|
321
|
+
switch (e.key) {
|
|
322
|
+
case 'ArrowRight':
|
|
323
|
+
case 'PageDown':
|
|
324
|
+
case ' ':
|
|
325
|
+
e.preventDefault();
|
|
326
|
+
send(curIdx + 1);
|
|
327
|
+
break;
|
|
328
|
+
case 'ArrowLeft':
|
|
329
|
+
case 'PageUp':
|
|
330
|
+
e.preventDefault();
|
|
331
|
+
send(curIdx - 1);
|
|
332
|
+
break;
|
|
333
|
+
case 'Home':
|
|
334
|
+
send(0);
|
|
335
|
+
break;
|
|
336
|
+
case 'End':
|
|
337
|
+
send(total - 1);
|
|
338
|
+
break;
|
|
339
|
+
case 'p':
|
|
340
|
+
case 'Escape':
|
|
341
|
+
window.close();
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
window.addEventListener('resize', () => {
|
|
346
|
+
fitPreview(cur.frame, cur.scale);
|
|
347
|
+
fitPreview(nxt.frame, nxt.scale);
|
|
348
|
+
});
|
|
349
|
+
window.setInterval(tick, 500);
|
|
350
|
+
// 本番ウィンドウに現在の状態を要求する
|
|
351
|
+
if (window.opener)
|
|
352
|
+
window.opener.postMessage({ ippon: 'hello' }, '*');
|
|
353
|
+
draw();
|
|
354
|
+
tick();
|
|
355
|
+
}
|
|
356
|
+
})();
|