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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ff93e8caf99520439378ec599dea98fe0cb16c6704fbd7180fdedbd62b3ec2f
4
- data.tar.gz: 0e10b33b011d5a227236da73dcb0ba8d576abb0f421010f832d0ba2e8372b582
3
+ metadata.gz: 85ec1823b674eea078278bc07b3516fc1cca469f3b00a42a7d53d677cf76306d
4
+ data.tar.gz: 2c6fe20e5c8749718b51cb2cadc40706cc610b75002c2ff061f9650733cdcc40
5
5
  SHA512:
6
- metadata.gz: 9fd07de540a9a83d403e4dd5b38bd71306e19287605a0c56819e43b1addfb683c1528f95732dc4273a33c20d7dee57b95b41b55562b2712b4d67591a662faee0
7
- data.tar.gz: b01f44dc7f6250766599fccbe388e816efd06b83d1ea098df3713049ba9db3cc6d55c8864150fe10ad4417778e5e029f341cf66d2bd7dab00989ada94bbdff73
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.9.gem
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
- スライドの読み込み完了から計測し、既定では5分で時間切れ画面を表示します。
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
- タイマーと時間切れ画面はPDFには含めません。
150
+ 開始待ち画面、タイマー、時間切れ画面はPDFには含めません。
145
151
 
146
152
  ## 同梱フォント
147
153
 
data/assets/build.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.1.9",
3
- "source_sha256": "45e449c831f3ae3092ad05f56b8694ae73c2c5ffcc1a5fa1469351eb2e108b52",
2
+ "version": "0.1.11",
3
+ "source_sha256": "7fd82f57dedeab0d552457b69e4a32432bfce7316ce41c8618808d34c001e82e",
4
4
  "loader_commit": "dc6b0ffd5800ed9bfc49268ed8021d456f10ddf8",
5
- "wasm_sha256": "64559bd712ffbafbea133c20cbd2a44ad2ad87410465c93766817ed858e0caee"
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
- <div class="time-expired-image" role="img" aria-label="兎柄フードをくわえるワニ(さめ)"></div>
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 * 1024 / 683)); aspect-ratio: 1024 / 683; flex-shrink: 0; background: url('/assets/time-expired.png') right center / 200% 100% no-repeat; }
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: 35166009 bytes
4
- sha256: 64559bd712ffbafbea133c20cbd2a44ad2ad87410465c93766817ed858e0caee
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
- start_timer
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
@@ -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
- title[:style][:display] = "none" if title[:textContent].to_s.empty?
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.png runtime.pr672.js wani.wasm runtime.LICENSE runtime.NOTICE build.json
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
@@ -1,3 +1,3 @@
1
1
  module Wani
2
- VERSION = "0.1.9"
2
+ VERSION = "0.1.11"
3
3
  end
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.9
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.png
103
+ - assets/time-expired.webp
104
104
  - assets/wani.wasm
105
105
  - exe/wani
106
106
  - lib/wani.rb
Binary file