wani 0.1.9 → 0.1.11
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.
- checksums.yaml +4 -4
- data/README.md +9 -3
- data/assets/build.json +3 -3
- data/assets/index.html +4 -1
- data/assets/style.css +11 -1
- data/assets/time-expired.webp +0 -0
- data/assets/wani.wasm +2 -2
- data/lib/wani/browser/application.rb +22 -2
- data/lib/wani/browser/layout.rb +37 -0
- data/lib/wani/browser/presentation_timer.rb +9 -0
- data/lib/wani/browser/renderer.rb +5 -1
- data/lib/wani/server/application.rb +1 -1
- data/lib/wani/version.rb +1 -1
- metadata +2 -2
- data/assets/time-expired.png +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85ec1823b674eea078278bc07b3516fc1cca469f3b00a42a7d53d677cf76306d
|
|
4
|
+
data.tar.gz: 2c6fe20e5c8749718b51cb2cadc40706cc610b75002c2ff061f9650733cdcc40
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0fe3730449dc7263365ea27e2fb84447c382896bec358ae4926aead67af4eb5fbf96cc1e13d0a2f055174948dd69ad6babd6af49f13331eb7d9f3a3ed3cb2804
|
|
7
|
+
data.tar.gz: 128750706c4ad65b2945348703d19d09606e30a4c24c1738bf3fe2974ed84622872469e4e1061079fb9d073e818e094e5586c1c415d82ed7237fa3cfdcd1afb9
|
data/README.md
CHANGED
|
@@ -25,7 +25,7 @@ npm ci
|
|
|
25
25
|
bundle exec rake build:wasm
|
|
26
26
|
|
|
27
27
|
gem build wani.gemspec
|
|
28
|
-
gem install ./wani-0.1.
|
|
28
|
+
gem install ./wani-0.1.11.gem
|
|
29
29
|
|
|
30
30
|
wani new my-talk
|
|
31
31
|
cd my-talk
|
|
@@ -111,6 +111,8 @@ notes:
|
|
|
111
111
|
|
|
112
112
|
スライドは1920×1080、16:9、白背景です。タイトル・本文・脚注を分け、画面に合わせて全体を拡大縮小します。
|
|
113
113
|
画像とフォントを読み込み、幅と高さに合わせて文字サイズを自動調整します。
|
|
114
|
+
タイトルは1行に収めます。本文の段落・見出し・箇条書きは、使用フォントの全角20文字(「あ」20個)相当の幅以下なら折り返さず、それを超える場合は自然に折り返します。
|
|
115
|
+
明示的な改行は維持し、改行ごとに幅を判定します。文字サイズは本文全体で共通です。
|
|
114
116
|
画像は比率を維持して縦に配置し、画像だけの場合は本文領域を広く使います。
|
|
115
117
|
収まらない内容は分割・スクロールせず、番号と内容を含む警告を表示します。
|
|
116
118
|
画像の読み込みを最大15秒待ち、失敗やタイムアウトを通知します。
|
|
@@ -128,7 +130,10 @@ Ctrl+P でも印刷できます。
|
|
|
128
130
|
|
|
129
131
|
## 発表タイマー
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
hashなしのURLで開くと、最初のスライドに半透明の黒いベールと「スタート」ボタンを表示します。
|
|
134
|
+
待機中はページ移動と計測を止め、ボタンを押すとベールが消えてタイマーが始まります。Fキーで全画面にして待機できます。
|
|
135
|
+
`#slide=1` などのhash付きURLでは、読み込み完了から計測します。
|
|
136
|
+
既定では5分で時間切れ画面を表示します。
|
|
132
137
|
Front Matter の `time_limit` に分数を指定して変更できます(小数も可)。
|
|
133
138
|
`time_limit: 0` でタイマーを無効にします。
|
|
134
139
|
|
|
@@ -136,12 +141,13 @@ Front Matter の `time_limit` に分数を指定して変更できます(小
|
|
|
136
141
|
残り5秒は中央に数字を表示し、画面の短辺に対して20%・40%・60%・80%・100%と強調サイズを増やします。
|
|
137
142
|
各数字は0.8秒で通常サイズに戻ります。動きを減らす設定では拡大アニメーションを省略します。
|
|
138
143
|
時間切れになると、兎柄フードをくわえたワニ(さめ)の画像を表示します。
|
|
144
|
+
画像は軽量なWebP形式で配布し、発表開始前に読み込みとデコードを済ませて、時間切れの文字と同時に表示します。
|
|
139
145
|
左右キーまたは「スライドに戻る」ボタンで、時間切れ直前のスライドに戻れます。
|
|
140
146
|
戻った後は通常通りページを移動でき、時間切れ画面は再表示しません。
|
|
141
147
|
最終スライドの表示中はタイマーを一時停止し、カウントダウンも時間切れ画面も表示しません。
|
|
142
148
|
前のスライドに戻ると、停止前の残り時間から再開します。1ページだけの場合も停止します。
|
|
143
149
|
ページを再読み込みするとタイマーも最初から始まります。
|
|
144
|
-
|
|
150
|
+
開始待ち画面、タイマー、時間切れ画面はPDFには含めません。
|
|
145
151
|
|
|
146
152
|
## 同梱フォント
|
|
147
153
|
|
data/assets/build.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1.
|
|
3
|
-
"source_sha256": "
|
|
2
|
+
"version": "0.1.11",
|
|
3
|
+
"source_sha256": "7fd82f57dedeab0d552457b69e4a32432bfce7316ce41c8618808d34c001e82e",
|
|
4
4
|
"loader_commit": "dc6b0ffd5800ed9bfc49268ed8021d456f10ddf8",
|
|
5
|
-
"wasm_sha256": "
|
|
5
|
+
"wasm_sha256": "777e59a959570126bfe466047c60b3e49ae72bb9a4b645c6564321cf9580ae08"
|
|
6
6
|
}
|
data/assets/index.html
CHANGED
|
@@ -24,10 +24,13 @@
|
|
|
24
24
|
<p id="status" role="status">Ruby を読み込んでいます…</p>
|
|
25
25
|
<aside id="warnings" role="status" hidden></aside>
|
|
26
26
|
<div id="viewport"><main id="stage" aria-label="スライド"></main></div>
|
|
27
|
+
<section id="start-overlay" role="dialog" aria-modal="true" aria-label="発表の開始" hidden>
|
|
28
|
+
<button id="start-presentation">スタート</button>
|
|
29
|
+
</section>
|
|
27
30
|
<output id="countdown" aria-live="off" aria-label="発表の残り時間" hidden></output>
|
|
28
31
|
<section id="time-expired" role="dialog" aria-modal="true" aria-labelledby="time-expired-title" hidden>
|
|
29
32
|
<h1 id="time-expired-title">時間切れです!</h1>
|
|
30
|
-
<
|
|
33
|
+
<img id="time-expired-image" class="time-expired-image" src="/assets/time-expired.webp" width="1086" height="724" alt="兎柄フードをくわえるワニ(さめ)">
|
|
31
34
|
<p>← または → キーで元のスライドに戻れます</p>
|
|
32
35
|
<button id="resume-slides">スライドに戻る</button>
|
|
33
36
|
</section>
|
data/assets/style.css
CHANGED
|
@@ -34,6 +34,10 @@ button:focus-visible, a:focus-visible { outline: 2px solid #557e64; outline-offs
|
|
|
34
34
|
.title { margin: 0 0 36px; white-space: nowrap; flex: 0 0 160px; font-weight: 700; line-height: 1.12; overflow: hidden; letter-spacing: -.025em; }
|
|
35
35
|
.body { flex: 1 1 0; min-height: 0; overflow: hidden; line-height: 1.3; }
|
|
36
36
|
.body p { margin: 0 0 .35em; }
|
|
37
|
+
.titleless .body { display: flex; flex-direction: column; justify-content: safe center; text-align: center; }
|
|
38
|
+
.titleless .body > * { flex-shrink: 0; }
|
|
39
|
+
.titleless .body > :last-child { margin-bottom: 0; }
|
|
40
|
+
.titleless .body img { margin-inline: auto; }
|
|
37
41
|
.body ul, .body ol { margin: .2em 0 .4em; padding-left: 1.35em; }
|
|
38
42
|
.body pre { margin: .3em 0; padding: .4em .55em; background: #f3f6f2; white-space: pre-wrap; tab-size: 2; }
|
|
39
43
|
.body code { font-family: ui-monospace, "IBM Plex Sans JP", monospace; font-size: .8em; }
|
|
@@ -52,6 +56,7 @@ button:focus-visible, a:focus-visible { outline: 2px solid #557e64; outline-offs
|
|
|
52
56
|
.bridge-wani { display: block; width: 100%; height: 44px; object-fit: contain; }
|
|
53
57
|
.rabbit-position { position: absolute; bottom: 30px; width: 84px; height: 40px; left: calc((100% - 84px) * var(--rabbit-progress)); }
|
|
54
58
|
.rabbit-image { display: block; width: 84px; height: 40px; }
|
|
59
|
+
.decorations[data-direction="left"] .rabbit-image { scale: -1 1; }
|
|
55
60
|
.slide.active .bridge-segment[data-appearing="true"] { animation: wani-arrive .35s ease-out; }
|
|
56
61
|
.slide.active .decorations[data-moving="true"] .rabbit-position { animation: rabbit-advance .45s ease-in-out; }
|
|
57
62
|
.slide.active .decorations[data-moving="true"] .rabbit-image { animation: rabbit-hop .45s ease-in-out; }
|
|
@@ -88,7 +93,7 @@ button:focus-visible, a:focus-visible { outline: 2px solid #557e64; outline-offs
|
|
|
88
93
|
#time-expired { position: fixed; inset: 0; z-index: 30; display: flex; flex-direction: column; align-items: center; justify-content: center; background: white; color: #20352e; padding: 16px; overflow: auto; }
|
|
89
94
|
#time-expired[hidden], #countdown[hidden] { display: none; }
|
|
90
95
|
#time-expired h1 { margin: 0; font-size: clamp(24px, 4vw, 56px); }
|
|
91
|
-
.time-expired-image { width: min(90vw, calc(65vh *
|
|
96
|
+
.time-expired-image { width: min(90vw, calc(65vh * 1086 / 724)); aspect-ratio: 1086 / 724; height: auto; flex-shrink: 0; }
|
|
92
97
|
#time-expired p { margin: 8px 0; text-align: center; }
|
|
93
98
|
@media print { #countdown, #time-expired { display: none !important; } }
|
|
94
99
|
|
|
@@ -101,3 +106,8 @@ button:focus-visible, a:focus-visible { outline: 2px solid #557e64; outline-offs
|
|
|
101
106
|
.island-left { left: 0; }
|
|
102
107
|
.island-right { right: 0; }
|
|
103
108
|
.rabbit-position { z-index: 2; }
|
|
109
|
+
|
|
110
|
+
#start-overlay { position: fixed; inset: 0; z-index: 30; display: flex; align-items: center; justify-content: center; background: rgb(0 0 0 / 60%); }
|
|
111
|
+
#start-overlay[hidden] { display: none; }
|
|
112
|
+
#start-presentation { padding: 18px 48px; font-size: clamp(24px, 4vw, 40px); font-weight: 700; border-radius: 12px; }
|
|
113
|
+
@media print { #start-overlay { display: none !important; } }
|
|
Binary file
|
data/assets/wani.wasm
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/assets/wani.wasm
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 35169057 bytes
|
|
4
|
+
sha256: 777e59a959570126bfe466047c60b3e49ae72bb9a4b645c6564321cf9580ae08
|
|
@@ -26,11 +26,19 @@ module Wani
|
|
|
26
26
|
@layout.wait_assets(element("stage"))
|
|
27
27
|
@sections.each_with_index { |section, index| @layout.fit_slide(section, index + 1) }
|
|
28
28
|
prepare_pages(false)
|
|
29
|
+
prepare_timer_image
|
|
29
30
|
bind_events
|
|
31
|
+
@waiting_to_start = JS.global[:location][:hash].to_s.empty?
|
|
30
32
|
go(hash_index)
|
|
31
33
|
%w[previous next fullscreen speaker print print-notes].each { |id| element(id)[:disabled] = false }
|
|
32
34
|
update_buttons
|
|
33
|
-
|
|
35
|
+
if @waiting_to_start
|
|
36
|
+
element("start-overlay")[:hidden] = false
|
|
37
|
+
element("viewport")[:inert] = true
|
|
38
|
+
element("start-presentation").focus
|
|
39
|
+
else
|
|
40
|
+
start_timer
|
|
41
|
+
end
|
|
34
42
|
element("status")[:textContent] = "準備できました"
|
|
35
43
|
end
|
|
36
44
|
|
|
@@ -55,6 +63,15 @@ module Wani
|
|
|
55
63
|
end
|
|
56
64
|
|
|
57
65
|
def bind_events
|
|
66
|
+
element("start-presentation").addEventListener("click") do
|
|
67
|
+
next unless @waiting_to_start
|
|
68
|
+
@waiting_to_start = false
|
|
69
|
+
element("start-overlay")[:hidden] = true
|
|
70
|
+
element("viewport")[:inert] = false
|
|
71
|
+
element("start-presentation").blur
|
|
72
|
+
go(0)
|
|
73
|
+
start_timer
|
|
74
|
+
end
|
|
58
75
|
element("previous").addEventListener("click") { go(@index - 1) }
|
|
59
76
|
element("next").addEventListener("click") { go(@index + 1) }
|
|
60
77
|
element("fullscreen").addEventListener("click") { fullscreen }
|
|
@@ -65,6 +82,7 @@ module Wani
|
|
|
65
82
|
target = event[:target]
|
|
66
83
|
next if event[:ctrlKey] == JS::True || event[:altKey] == JS::True || event[:metaKey] == JS::True
|
|
67
84
|
next if %w[INPUT TEXTAREA SELECT].include?(target[:tagName].to_s) || target[:isContentEditable] == JS::True
|
|
85
|
+
next if @waiting_to_start && !%w[f F Escape n N].include?(event[:key].to_s)
|
|
68
86
|
handled = true
|
|
69
87
|
case event[:key].to_s
|
|
70
88
|
when "ArrowRight", " ", "PageDown" then go(@index + 1)
|
|
@@ -89,6 +107,8 @@ module Wani
|
|
|
89
107
|
end
|
|
90
108
|
|
|
91
109
|
def go(index)
|
|
110
|
+
return if @waiting_to_start && @active_number
|
|
111
|
+
|
|
92
112
|
if @time_expired
|
|
93
113
|
@time_expired = false
|
|
94
114
|
element("time-expired")[:hidden] = true
|
|
@@ -102,7 +122,7 @@ module Wani
|
|
|
102
122
|
@active_number = @index + 1
|
|
103
123
|
element("position")[:textContent] = "#{@index + 1} / #{@sections.length}"
|
|
104
124
|
desired = "#slide=#{@index + 1}"
|
|
105
|
-
JS.global[:history].replaceState(JS::Null, "", desired) unless JS.global[:location][:hash].to_s == desired
|
|
125
|
+
JS.global[:history].replaceState(JS::Null, "", desired) unless @waiting_to_start || JS.global[:location][:hash].to_s == desired
|
|
106
126
|
update_buttons
|
|
107
127
|
@layout.scale
|
|
108
128
|
sync_speaker
|
data/lib/wani/browser/layout.rb
CHANGED
|
@@ -7,6 +7,7 @@ module Wani
|
|
|
7
7
|
BODY_MAX = 160
|
|
8
8
|
TITLE_MIN = 24
|
|
9
9
|
TITLE_MAX = 220
|
|
10
|
+
NO_WRAP_EMS = 20
|
|
10
11
|
|
|
11
12
|
def initialize(document, &warn)
|
|
12
13
|
@document, @warn = document, warn
|
|
@@ -55,6 +56,7 @@ module Wani
|
|
|
55
56
|
@warn.call("スライド #{number}: タイトルが収まりません: #{title[:textContent]}") unless fits?(title)
|
|
56
57
|
end
|
|
57
58
|
body = section.querySelector(".body")
|
|
59
|
+
prepare_wrapping(body)
|
|
58
60
|
fit_images(body)
|
|
59
61
|
fit_font(body, BODY_MIN, BODY_MAX)
|
|
60
62
|
@warn.call("スライド #{number}: 本文が収まりません: #{body[:textContent].to_s[0, 120]}") unless fits?(body)
|
|
@@ -72,6 +74,41 @@ module Wani
|
|
|
72
74
|
|
|
73
75
|
private
|
|
74
76
|
|
|
77
|
+
def prepare_wrapping(body)
|
|
78
|
+
# Rebuild the wrappers when a slide is fitted again (for example after
|
|
79
|
+
# editing). Ranges preserve links, emphasis and explicit line breaks.
|
|
80
|
+
body.querySelectorAll("[data-wani-line]").to_a.each do |line|
|
|
81
|
+
line.replaceWith(*line[:childNodes].to_a)
|
|
82
|
+
end
|
|
83
|
+
body[:style][:fontSize] = "#{BODY_MIN}px"
|
|
84
|
+
body.querySelectorAll("p, h1, h2, h3, h4, h5, h6, li").to_a.each do |block|
|
|
85
|
+
next unless block.querySelector("p, ul, ol") == JS::Null
|
|
86
|
+
breaks = block.querySelectorAll("br").to_a
|
|
87
|
+
# Work backwards so extracting a range does not invalidate the next.
|
|
88
|
+
(0..breaks.length).to_a.reverse_each do |index|
|
|
89
|
+
range = @document.createRange
|
|
90
|
+
range.selectNodeContents(block)
|
|
91
|
+
range.setStartAfter(breaks[index - 1]) if index.positive?
|
|
92
|
+
range.setEndBefore(breaks[index]) if index < breaks.length
|
|
93
|
+
line = @document.createElement("span")
|
|
94
|
+
line.setAttribute("data-wani-line", "")
|
|
95
|
+
line[:style][:whiteSpace] = "nowrap"
|
|
96
|
+
line.appendChild(range.extractContents)
|
|
97
|
+
range.insertNode(line)
|
|
98
|
+
|
|
99
|
+
reference = @document.createElement("span")
|
|
100
|
+
reference[:textContent] = "あ" * NO_WRAP_EMS
|
|
101
|
+
reference[:style][:position] = "absolute"
|
|
102
|
+
reference[:style][:whiteSpace] = "nowrap"
|
|
103
|
+
block.appendChild(reference)
|
|
104
|
+
if line.getBoundingClientRect[:width].to_f > reference.getBoundingClientRect[:width].to_f + 0.01
|
|
105
|
+
line[:style][:whiteSpace] = "normal"
|
|
106
|
+
end
|
|
107
|
+
reference.remove
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
75
112
|
def fits?(node)
|
|
76
113
|
node[:scrollWidth].to_i <= node[:clientWidth].to_i + 1 && node[:scrollHeight].to_i <= node[:clientHeight].to_i + 1
|
|
77
114
|
end
|
|
@@ -3,6 +3,15 @@ module Wani
|
|
|
3
3
|
class Application
|
|
4
4
|
private
|
|
5
5
|
|
|
6
|
+
# Decode the actual displayed image before enabling presentation controls.
|
|
7
|
+
def prepare_timer_image
|
|
8
|
+
return if @presentation.time_limit.zero?
|
|
9
|
+
|
|
10
|
+
element("time-expired-image").decode.await
|
|
11
|
+
rescue JS::Error
|
|
12
|
+
warn("時間切れの画像を読み込めませんでした")
|
|
13
|
+
end
|
|
14
|
+
|
|
6
15
|
def start_timer
|
|
7
16
|
return if @presentation.time_limit.zero?
|
|
8
17
|
|
|
@@ -13,7 +13,10 @@ module Wani
|
|
|
13
13
|
section.setAttribute("aria-label", "スライド #{slide.number}")
|
|
14
14
|
title = child(section, "h1", "title")
|
|
15
15
|
title[:textContent] = notes ? "ノート #{slide.number}" : slide.title
|
|
16
|
-
|
|
16
|
+
if title[:textContent].to_s.empty?
|
|
17
|
+
title[:style][:display] = "none"
|
|
18
|
+
section[:className] = "slide titleless"
|
|
19
|
+
end
|
|
17
20
|
body = child(section, "div", "body")
|
|
18
21
|
body[:innerHTML] = notes ? slide.notes_html : slide.html
|
|
19
22
|
resolve_links(body)
|
|
@@ -53,6 +56,7 @@ module Wani
|
|
|
53
56
|
current = BridgeProgress.new(scene.getAttribute("data-slide").to_s.to_i, @presentation.slides.length)
|
|
54
57
|
scene[:style].setProperty("--rabbit-from", previous.rabbit_fraction.to_s)
|
|
55
58
|
scene.setAttribute("data-moving", (previous.rabbit_fraction != current.rabbit_fraction).to_s)
|
|
59
|
+
scene.setAttribute("data-direction", current.rabbit_fraction < previous.rabbit_fraction ? "left" : "right")
|
|
56
60
|
scene.querySelectorAll(".bridge-segment").to_a.each_with_index do |segment, index|
|
|
57
61
|
appearing = index >= BridgeProgress::SEGMENTS - current.sharks && index < BridgeProgress::SEGMENTS - previous.sharks
|
|
58
62
|
segment.setAttribute("data-appearing", appearing.to_s)
|
|
@@ -7,7 +7,7 @@ module Wani
|
|
|
7
7
|
module Server
|
|
8
8
|
# Only the selected Markdown, images below its directory and fixed gem assets are public.
|
|
9
9
|
class Application
|
|
10
|
-
ASSETS = %w[index.html style.css highlight.css rouge.LICENSE bootstrap.rb speaker.html rabbit.svg shark.svg time-expired.
|
|
10
|
+
ASSETS = %w[index.html style.css highlight.css rouge.LICENSE bootstrap.rb speaker.html rabbit.svg shark.svg time-expired.webp runtime.pr672.js wani.wasm runtime.LICENSE runtime.NOTICE build.json
|
|
11
11
|
fonts/IBMPlexSansJP-Regular.woff2 fonts/IBMPlexSansJP-Bold.woff2].freeze
|
|
12
12
|
TYPES = { ".html" => "text/html; charset=utf-8", ".css" => "text/css; charset=utf-8", ".rb" => "text/plain; charset=utf-8", ".js" => "text/javascript", ".wasm" => "application/wasm", ".md" => "text/plain; charset=utf-8", ".svg" => "image/svg+xml", ".png" => "image/png", ".jpg" => "image/jpeg", ".jpeg" => "image/jpeg", ".webp" => "image/webp", ".json" => "application/json", ".woff2" => "font/woff2" }.freeze
|
|
13
13
|
IMAGES = %w[.png .jpg .jpeg .webp .svg].freeze
|
data/lib/wani/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wani
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ledsun
|
|
@@ -100,7 +100,7 @@ files:
|
|
|
100
100
|
- assets/shark.svg
|
|
101
101
|
- assets/speaker.html
|
|
102
102
|
- assets/style.css
|
|
103
|
-
- assets/time-expired.
|
|
103
|
+
- assets/time-expired.webp
|
|
104
104
|
- assets/wani.wasm
|
|
105
105
|
- exe/wani
|
|
106
106
|
- lib/wani.rb
|
data/assets/time-expired.png
DELETED
|
Binary file
|