@johnmorrisdotca/kyogijo-stadiums 0.6.2 → 0.8.0

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +2 -2
  3. package/dist/budo/looks.js +1 -1
  4. package/dist/budo/room.d.ts +34 -2
  5. package/dist/budo/room.js +39 -7
  6. package/dist/budo/types.d.ts +6 -1
  7. package/dist/budo.style.d.ts +1 -1
  8. package/dist/budo.style.js +6 -3
  9. package/dist/charts-entry.d.ts +0 -3
  10. package/dist/charts-entry.js +1 -3
  11. package/dist/crowd.d.ts +4 -0
  12. package/dist/crowd.draw.d.ts +5 -0
  13. package/dist/crowd.draw.js +34 -5
  14. package/dist/crowd.js +6 -1
  15. package/dist/draw.js +6 -3
  16. package/dist/element.d.ts +3 -3
  17. package/dist/element.js +16 -2
  18. package/dist/halls.js +4 -3
  19. package/dist/layout.js +1 -2
  20. package/dist/mark.d.ts +1 -1
  21. package/dist/mark.js +1 -1
  22. package/dist/strings.d.ts +7 -29
  23. package/dist/strings.js +6 -24
  24. package/dist/style.js +1 -2
  25. package/dist/surfaces/budo.js +22 -2
  26. package/dist/turn-entry.d.ts +2 -0
  27. package/dist/turn-entry.js +1 -0
  28. package/dist/turn.dom.d.ts +8 -0
  29. package/dist/turn.dom.js +147 -8
  30. package/dist/turn.kit.d.ts +5 -3
  31. package/dist/turn.kit.js +1 -1
  32. package/dist/turn.markup.d.ts +8 -1
  33. package/dist/turn.markup.js +18 -4
  34. package/dist/turn.model.d.ts +26 -0
  35. package/dist/turn.model.js +34 -3
  36. package/dist/turn.style.js +58 -4
  37. package/dist/uprights.d.ts +7 -0
  38. package/dist/uprights.js +15 -1
  39. package/dist/venue.d.ts +31 -0
  40. package/dist/venue.geom.d.ts +53 -0
  41. package/dist/venue.geom.js +225 -0
  42. package/dist/venue.js +172 -0
  43. package/dist/venue.room.d.ts +9 -0
  44. package/dist/venue.room.js +240 -0
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +1 -1
  48. package/dist/road.d.ts +0 -26
  49. package/dist/road.draw.d.ts +0 -6
  50. package/dist/road.draw.js +0 -270
  51. package/dist/road.js +0 -60
  52. package/dist/road.model.d.ts +0 -63
  53. package/dist/road.model.js +0 -215
  54. package/dist/road.style.d.ts +0 -7
  55. package/dist/road.style.js +0 -26
  56. package/dist/road.types.d.ts +0 -125
  57. package/dist/road.types.js +0 -1
package/dist/mark.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // The round team mark: a flag or a logo clipped to a circle with a thin ring, or, when a team has no picture, its initials on its colour. One function draws it for the
2
- // score on a surface (a banner at each end, the badge and the strip under a compact card) and for the road-to-the-final chart, so a country has the same mark everywhere.
2
+ // score on a surface (a banner at each end, the badge and the strip under a compact card), so a country has the same mark everywhere.
3
3
  import { inkOn, safeColour } from "./colour.js";
4
4
  import { el, esc, num } from "./format.js";
5
5
  import { textWidth } from "./text.js";
package/dist/strings.d.ts CHANGED
@@ -901,34 +901,6 @@ export interface KyogijoWords {
901
901
  cell: string;
902
902
  listSep: string;
903
903
  };
904
- /**
905
- * The words of the road-to-the-final chart (`roadToFinal`): its title when the caller gives none, the names of the soccer knockout rounds (any other round is named by
906
- * the caller, or by its own key), how a match was decided (`ft`, `aet`, `pens` with {a} and {b}), the results, the table's headings and caption, and the summary.
907
- */
908
- road: {
909
- title: string;
910
- vs: string;
911
- group: string;
912
- rounds: Record<"round-of-32" | "round-of-16" | "quarter-final" | "semi-final" | "final", string>;
913
- ft: string;
914
- aet: string;
915
- pens: string;
916
- won: string;
917
- lost: string;
918
- drew: string;
919
- tableTeam: string;
920
- tableRound: string;
921
- tableOpponent: string;
922
- tableScore: string;
923
- tableDecided: string;
924
- tableResult: string;
925
- caption: string;
926
- name: string;
927
- summary: string;
928
- final: string;
929
- champion: string;
930
- toPlay: string;
931
- };
932
904
  /** The words of a replay: its summary, its marks, what is announced when something happens, and the controls under it. */
933
905
  replay: {
934
906
  /** The first sentence of the summary: {time} is the length. */
@@ -1004,6 +976,12 @@ export interface KyogijoWords {
1004
976
  lower: string;
1005
977
  zoomIn: string;
1006
978
  zoomOut: string;
979
+ /** The auto-turn: the button's name (its state is `aria-pressed`), what its tooltip says to press for, and what is announced when it starts and stops. */
980
+ play: string;
981
+ playOn: string;
982
+ playOff: string;
983
+ announcePlay: string;
984
+ announcePause: string;
1007
985
  /** What is announced after a key or a button: {name} for a viewpoint, and the angles: {yaw} and {pitch} in degrees. */
1008
986
  announce: string;
1009
987
  announceAngle: string;
@@ -1015,7 +993,7 @@ export interface KyogijoWords {
1015
993
  item: string;
1016
994
  itemSag: string;
1017
995
  /** The names of what stands on the surface, by kind. */
1018
- parts: Record<"scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets", string>;
996
+ parts: Record<"scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets" | "stands" | "wall-seats" | "roof" | "gallery" | "dais" | "banner" | "shrine" | "mat-edge", string>;
1019
997
  };
1020
998
  /** The names of the zones that `zoneAt` answers with (rules.ts), by id. */
1021
999
  rules: Record<RuleId, string>;
package/dist/strings.js CHANGED
@@ -414,16 +414,6 @@ export const KYOGIJO_STRINGS = {
414
414
  },
415
415
  },
416
416
  grid: { title: "A grid of drawings ({n})", drawings: "Drawings ({n}): {list}.", key: "Key: {list}.", cell: "Drawing {n} of {total}", listSep: "; " },
417
- road: {
418
- title: "Road to the final", vs: "v", group: "Group stage",
419
- rounds: { "round-of-32": "Round of 32", "round-of-16": "Round of 16", "quarter-final": "Quarter-final", "semi-final": "Semi-final", final: "Final" },
420
- ft: "FT", aet: "AET", pens: "{a}-{b} pens", won: "Won", lost: "Lost", drew: "Drew",
421
- tableTeam: "Team", tableRound: "Round", tableOpponent: "Opponent", tableScore: "Score", tableDecided: "Decided", tableResult: "Result",
422
- caption: "{a} and {b}: every match on the way to the final. Scores are written with the team in the first column first.",
423
- name: "{a} and {b}: {title}",
424
- summary: "{a} played {an} matches on the way to the final and {b} played {bn}.",
425
- final: "The final: {a} {score} {b} ({how}).", champion: "{name} won the final.", toPlay: "The final is still to be played.",
426
- },
427
417
  replay: {
428
418
  title: "Replay, {time}", objects: "{n} tracked objects", objectsOne: "{n} tracked object", events: "{n} events", eventsOne: "{n} event", players: "Players", others: "Ball, puck and officials", trail: "Trail of the last {s} seconds", numbers: "Shirt numbers", eventLines: "Passes and shots",
429
419
  home: "home", away: "away", ball: "ball", puck: "puck", official: "official", pass: "Pass", shot: "Shot", goal: "Goal", whistle: "Whistle",
@@ -435,13 +425,14 @@ export const KYOGIJO_STRINGS = {
435
425
  },
436
426
  turn: {
437
427
  region: "3D view", group: "Viewpoint",
438
- keys: "A 3D view you can turn. Drag, or use the arrow keys, to turn and tilt it; plus and minus zoom; 1 to 4 pick a viewpoint; 0 or Home puts it back. Pinch, or hold Ctrl and scroll, to zoom.",
428
+ keys: "A 3D view you can turn. Drag, or use the arrow keys, to turn and tilt it; plus and minus zoom; 1 to 4 pick a viewpoint; 0 or Home puts it back; Space starts and stops the auto-turn. Pinch, or hold Ctrl and scroll, to zoom.",
439
429
  overhead: "Overhead", broadcast: "Broadcast", end: "End-on", corner: "Low corner",
440
430
  reset: "Reset the view", turnLeft: "Turn left", turnRight: "Turn right", raise: "Tip the surface away", lower: "Tip the surface toward you", zoomIn: "Zoom in", zoomOut: "Zoom out",
431
+ play: "Auto-turn", playOn: "Start the auto-turn", playOff: "Pause the auto-turn", announcePlay: "Auto-turn on", announcePause: "Auto-turn paused",
441
432
  announce: "{name} view", announceAngle: "Turned to {yaw} degrees, seen from {pitch} degrees above the floor.",
442
433
  flat: "This browser cannot draw the 3D view, so the flat drawing is shown.",
443
434
  standing: "Standing on it, in the 3D view: {list}.", item: "{name} {h}", itemSag: "{name} {h} at the posts, {low} at the middle",
444
- parts: { scoreboard: "scoreboard (lower edge)", net: "net", antennae: "antennae", goalposts: "goal posts", netposts: "net posts", hoop: "baskets", goal: "goals", boards: "boards and glass", stumps: "stumps", pylons: "pylons", flags: "corner flags", fence: "fence", backstop: "backstop", "foul-poles": "foul poles", "table-legs": "table top", walls: "walls", "starting-blocks": "starting blocks", "backstroke-flags": "backstroke flags", "false-start-rope": "false start rope", springboards: "springboards", platforms: "platforms", "judges-stand": "judges' stand", hurdles: "hurdles", steeplechase: "steeplechase barriers", "high-jump": "high jump uprights and bar", "pole-vault": "pole vault uprights and bar", cage: "discus and hammer cage", padding: "padding", "rink-boards": "boards", mound: "clay mound", azuchi: "azuchi", targets: "targets" },
435
+ parts: { scoreboard: "scoreboard (lower edge)", net: "net", antennae: "antennae", goalposts: "goal posts", netposts: "net posts", hoop: "baskets", goal: "goals", boards: "boards and glass", stumps: "stumps", pylons: "pylons", flags: "corner flags", fence: "fence", backstop: "backstop", "foul-poles": "foul poles", "table-legs": "table top", walls: "walls", "starting-blocks": "starting blocks", "backstroke-flags": "backstroke flags", "false-start-rope": "false start rope", springboards: "springboards", platforms: "platforms", "judges-stand": "judges' stand", hurdles: "hurdles", steeplechase: "steeplechase barriers", "high-jump": "high jump uprights and bar", "pole-vault": "pole vault uprights and bar", cage: "discus and hammer cage", padding: "padding", "rink-boards": "boards", mound: "clay mound", azuchi: "azuchi", targets: "targets", stands: "stands", "wall-seats": "seats on top of the wall", roof: "roof", gallery: "gallery", dais: "dais", banner: "banner", shrine: "shrine", "mat-edge": "mat thickness" },
445
436
  },
446
437
  rules: {
447
438
  "defensive-zone": "Defensive zone",
@@ -959,16 +950,6 @@ export const KYOGIJO_STRINGS = {
959
950
  },
960
951
  },
961
952
  grid: { title: "図のグリッド({n}枚)", drawings: "図({n}枚):{list}。", key: "凡例:{list}。", cell: "{total}枚中{n}枚目の図", listSep: ";" },
962
- road: {
963
- title: "決勝への道", vs: "対", group: "グループステージ",
964
- rounds: { "round-of-32": "ラウンド32", "round-of-16": "ラウンド16", "quarter-final": "準々決勝", "semi-final": "準決勝", final: "決勝" },
965
- ft: "90分", aet: "延長", pens: "PK {a}-{b}", won: "勝ち", lost: "負け", drew: "引き分け",
966
- tableTeam: "チーム", tableRound: "ラウンド", tableOpponent: "対戦相手", tableScore: "スコア", tableDecided: "決着", tableResult: "結果",
967
- caption: "{a}と{b}:決勝までの全試合。スコアは「チーム」列のチームの得点を先に書いています。",
968
- name: "{a}と{b}:{title}",
969
- summary: "{a}は決勝までに{an}試合、{b}は{bn}試合を戦いました。",
970
- final: "決勝は{a} {score} {b}({how})でした。", champion: "{name}が優勝しました。", toPlay: "決勝はまだ行われていません。",
971
- },
972
953
  replay: {
973
954
  title: "リプレイ(再生時間{time})", objects: "追跡対象は{n}個", objectsOne: "追跡対象は{n}個", events: "イベント{n}件", eventsOne: "イベント{n}件", players: "選手", others: "ボール・パック・審判", trail: "直近{s}秒の軌跡", numbers: "背番号", eventLines: "パスとシュート",
974
955
  home: "ホーム", away: "アウェイ", ball: "ボール", puck: "パック", official: "審判", pass: "パス", shot: "シュート", goal: "ゴール", whistle: "ホイッスル",
@@ -980,13 +961,14 @@ export const KYOGIJO_STRINGS = {
980
961
  },
981
962
  turn: {
982
963
  region: "3D表示", group: "視点",
983
- keys: "回して見られる3D表示です。ドラッグするか矢印キーで回転と傾きを変え、プラスとマイナスのキーで拡大と縮小、1から4のキーで視点を選び、0キーまたはHomeキーで元に戻します。ピンチ操作、またはCtrlキーを押しながらのスクロールでも拡大・縮小できます。",
964
+ keys: "回して見られる3D表示です。ドラッグするか矢印キーで回転と傾きを変え、プラスとマイナスのキーで拡大と縮小、1から4のキーで視点を選び、0キーまたはHomeキーで元に戻し、スペースキーで自動回転を始めたり止めたりします。ピンチ操作、またはCtrlキーを押しながらのスクロールでも拡大・縮小できます。",
984
965
  overhead: "真上", broadcast: "中継カメラ", end: "エンド側から", corner: "低い角度から",
985
966
  reset: "元に戻す", turnLeft: "左へ回す", turnRight: "右へ回す", raise: "床面を奥へ傾ける", lower: "床面を手前へ傾ける", zoomIn: "拡大", zoomOut: "縮小",
967
+ play: "自動回転", playOn: "自動回転を始める", playOff: "自動回転を一時停止する", announcePlay: "自動回転を始めました", announcePause: "自動回転を一時停止しました",
986
968
  announce: "{name}の視点", announceAngle: "向きは{yaw}度、見下ろす角度は{pitch}度です。",
987
969
  flat: "このブラウザーでは3D表示ができないため、平面の図を表示しています。",
988
970
  standing: "3D表示で立って見えるもの:{list}。", item: "{name} {h}", itemSag: "{name} 支柱の位置で{h}(中央は{low})",
989
- parts: { scoreboard: "スコアボードの下端", net: "ネット", antennae: "アンテナ", goalposts: "ゴールポスト", netposts: "支柱", hoop: "バスケットゴール", goal: "ゴール", boards: "ボードとガラス", stumps: "ウィケットのスタンプ", pylons: "パイロン", flags: "コーナーフラッグ", fence: "外野フェンス", backstop: "バックネット", "foul-poles": "ファウルポール", "table-legs": "天板", walls: "壁", "starting-blocks": "スタート台", "backstroke-flags": "背泳ぎ用フラッグ", "false-start-rope": "フォルススタートロープ", springboards: "飛板", platforms: "固定台", "judges-stand": "ジャッジ台", hurdles: "ハードル", steeplechase: "障害物", "high-jump": "走高跳の支柱とバー", "pole-vault": "棒高跳の支柱とバー", cage: "円盤投・ハンマー投の囲い", padding: "防護マット", "rink-boards": "フェンス", mound: "土の台", azuchi: "安土", targets: "的" },
971
+ parts: { scoreboard: "スコアボードの下端", net: "ネット", antennae: "アンテナ", goalposts: "ゴールポスト", netposts: "支柱", hoop: "バスケットゴール", goal: "ゴール", boards: "ボードとガラス", stumps: "ウィケットのスタンプ", pylons: "パイロン", flags: "コーナーフラッグ", fence: "外野フェンス", backstop: "バックネット", "foul-poles": "ファウルポール", "table-legs": "天板", walls: "壁", "starting-blocks": "スタート台", "backstroke-flags": "背泳ぎ用フラッグ", "false-start-rope": "フォルススタートロープ", springboards: "飛板", platforms: "固定台", "judges-stand": "ジャッジ台", hurdles: "ハードル", steeplechase: "障害物", "high-jump": "走高跳の支柱とバー", "pole-vault": "棒高跳の支柱とバー", cage: "円盤投・ハンマー投の囲い", padding: "防護マット", "rink-boards": "フェンス", mound: "土の台", azuchi: "安土", targets: "的", stands: "スタンド", "wall-seats": "壁の上の観客席", roof: "屋根", gallery: "見学席", dais: "上座の台", banner: "垂れ幕", shrine: "神棚", "mat-edge": "畳の厚み" },
990
972
  },
991
973
  rules: {
992
974
  "defensive-zone": "ディフェンディングゾーン",
package/dist/style.js CHANGED
@@ -12,7 +12,6 @@ import { KYOGIJO_LOOK_STYLE } from "./look.js";
12
12
  import { KYOGIJO_MARK_STYLE } from "./mark.js";
13
13
  import { KYOGIJO_PAGEANT_MOTION, KYOGIJO_PAGEANT_STYLE } from "./pageant.style.js";
14
14
  import { KYOGIJO_POOL_STYLE } from "./pool.style.js";
15
- import { KYOGIJO_ROAD_STYLE } from "./road.style.js";
16
15
  import { KYOGIJO_SITUATION_STYLE } from "./situation.style.js";
17
16
  import { KYOGIJO_SPECIAL_STYLE } from "./special.style.js";
18
17
  /**
@@ -191,4 +190,4 @@ export const KYOGIJO_NOTE_STYLE = `
191
190
  .kyogijo .ky-an-ar{stroke:var(--c);fill:none}
192
191
  .kyogijo .ky-an-hd{fill:var(--c)}`.trim();
193
192
  /** Every rule of the package, for a page that draws with `style: "none"` and supplies the stylesheet itself. */
194
- export const KYOGIJO_STYLE = `${KYOGIJO_BASE_STYLE}\n${KYOGIJO_HEAT_STYLE}\n${KYOGIJO_TEAM_STYLE}\n${KYOGIJO_CURLING_STYLE}\n${KYOGIJO_FOLD_STYLE}\n${KYOGIJO_WALLS_STYLE}\n${KYOGIJO_COURTS_STYLE}\n${KYOGIJO_FIELDS_STYLE}\n${KYOGIJO_POOL_STYLE}\n${KYOGIJO_ATHLETICS_STYLE}\n${KYOGIJO_SKATING_STYLE}\n${KYOGIJO_BUDO_STYLE}\n${KYOGIJO_LOOK_STYLE}\n${KYOGIJO_HARDWOOD_STYLE}\n${KYOGIJO_TRACK_STYLE}\n${KYOGIJO_FEATURE_STYLE}\n${KYOGIJO_NOTE_STYLE}\n${KYOGIJO_SPECIAL_STYLE}\n${KYOGIJO_BACKGROUND_STYLE}\n${KYOGIJO_CROWD_STYLE}\n${KYOGIJO_GRID_STYLE}\n${KYOGIJO_MARK_STYLE}\n${KYOGIJO_PAGEANT_STYLE}\n${KYOGIJO_PAGEANT_MOTION}\n${KYOGIJO_SITUATION_STYLE}\n${KYOGIJO_ROAD_STYLE}`;
193
+ export const KYOGIJO_STYLE = `${KYOGIJO_BASE_STYLE}\n${KYOGIJO_HEAT_STYLE}\n${KYOGIJO_TEAM_STYLE}\n${KYOGIJO_CURLING_STYLE}\n${KYOGIJO_FOLD_STYLE}\n${KYOGIJO_WALLS_STYLE}\n${KYOGIJO_COURTS_STYLE}\n${KYOGIJO_FIELDS_STYLE}\n${KYOGIJO_POOL_STYLE}\n${KYOGIJO_ATHLETICS_STYLE}\n${KYOGIJO_SKATING_STYLE}\n${KYOGIJO_BUDO_STYLE}\n${KYOGIJO_LOOK_STYLE}\n${KYOGIJO_HARDWOOD_STYLE}\n${KYOGIJO_TRACK_STYLE}\n${KYOGIJO_FEATURE_STYLE}\n${KYOGIJO_NOTE_STYLE}\n${KYOGIJO_SPECIAL_STYLE}\n${KYOGIJO_BACKGROUND_STYLE}\n${KYOGIJO_CROWD_STYLE}\n${KYOGIJO_GRID_STYLE}\n${KYOGIJO_MARK_STYLE}\n${KYOGIJO_PAGEANT_STYLE}\n${KYOGIJO_PAGEANT_MOTION}\n${KYOGIJO_SITUATION_STYLE}`;
@@ -1,10 +1,13 @@
1
1
  // A budō surface lying wide, from its plan (budo/plan.ts): the floor and the free zone, the mats in their tones with their heri and their seams, the walls, the clay and the straw of a dohyō, the tape,
2
2
  // the targets. The plan is in metres; the drawing is in feet, as every surface lies, so the whole is drawn inside a group that scales metres to feet and moves the surface's box to the origin.
3
- import { el } from "../format.js";
3
+ import { el, num } from "../format.js";
4
+ import { floorTiles } from "../hardwood.js";
4
5
  import { areaD, marks, strokes } from "./field.js";
5
6
  import { atEnds, edges, ft, quarterTurn } from "./surface.js";
6
7
  /** Feet in a metre. */
7
8
  export const K = 1 / 0.3048;
9
+ /** The hardwood of a dojo's surround, laid as the basketball floor is (`hardwood.ts`): strip boards of 0.3 ft, each with a tone of its own and a little grain, seams that are only just there. */
10
+ const SURROUND_WOOD = { seed: 11, fine: true, board: 0.3, stain: 0, sheen: 0, hoops: false, bench: false };
8
11
  const feetBox = (b, ox, oy) => edges((b.x0 - ox) * K, (b.y0 - oy) * K, (b.x1 - ox) * K, (b.y1 - oy) * K);
9
12
  /** A plan's named range in feet, as a surface lists them. */
10
13
  function rangeOf(r, ox, oy) {
@@ -28,7 +31,7 @@ export function budoSurface(o, plan, margin) {
28
31
  const weave = full && plan.texture && pieces.length > 0;
29
32
  const stripe = (at, colour, opacity, width) => el("stop", { offset: at, "stop-color": colour, "stop-opacity": 0 }) + el("stop", { offset: at, "stop-color": colour, "stop-opacity": opacity }) + el("stop", { offset: at + width, "stop-color": colour, "stop-opacity": opacity }) + el("stop", { offset: at + width, "stop-color": colour, "stop-opacity": 0 });
30
33
  const woven = () => {
31
- defs += el("linearGradient", { id: `${o.id}-weave`, x1: 0, y1: 0, x2: 0, y2: 0.1, gradientUnits: "userSpaceOnUse", spreadMethod: "repeat" }, stripe(0.1, "#000000", 0.16, 0.22) + stripe(0.52, "#ffffff", 0.14, 0.16));
34
+ defs += el("linearGradient", { id: `${o.id}-weave`, x1: 0, y1: 0, x2: 0, y2: 0.1, gradientUnits: "userSpaceOnUse", spreadMethod: "repeat" }, stripe(0.1, "#000000", 0.03, 0.22) + stripe(0.52, "#000000", 0.015, 0.16));
32
35
  const rect = (p) => `M${p.x0} ${p.y0}H${p.x1}V${p.y1}H${p.x0}Z`;
33
36
  // A mat that lies across is drawn in a group turned a quarter, with its corners turned back, so that its ridges run along it.
34
37
  const turned = (p) => `M${p.y0} ${-p.x0}H${p.y1}V${-p.x1}H${p.y0}Z`;
@@ -43,6 +46,19 @@ export function budoSurface(o, plan, margin) {
43
46
  out += el("path", { class: "ky-bud-edge", d: pieces.map(rect).join(""), fill: "none" });
44
47
  return out;
45
48
  };
49
+ // The surround is polished hardwood: the boards (under) and their seams (over) as two tiles of the basketball floor's code, in feet and turned to metres, and a warm satin sheen in a few
50
+ // broad diagonal glints over them. Drawn only at full detail.
51
+ let polished = false;
52
+ const polish = (d) => {
53
+ const tiles = floorTiles(SURROUND_WOOD);
54
+ const tile = (suffix, body) => el("pattern", { id: `${o.id}-wood-${suffix}`, width: 96, height: num(tiles.height), patternUnits: "userSpaceOnUse", patternTransform: `scale(${(1 / K).toFixed(6)})` }, body);
55
+ const b = plan.box;
56
+ const glint = (at, w, a) => el("stop", { offset: at - w, "stop-color": "#fff1cf", "stop-opacity": 0 }) + el("stop", { offset: at, "stop-color": "#fff1cf", "stop-opacity": a }) + el("stop", { offset: at + w, "stop-color": "#fff1cf", "stop-opacity": 0 });
57
+ defs += tile("a", tiles.under) + tile("b", tiles.over) + el("linearGradient", { id: `${o.id}-wood-s`, gradientUnits: "userSpaceOnUse", x1: b.x0, y1: b.y0, x2: b.x1, y2: b.y1 }, glint(0.22, 0.06, 0.1) + glint(0.5, 0.08, 0.13) + glint(0.78, 0.05, 0.08));
58
+ const layer = (suffix, opacity) => el("path", { class: "ky-bud-wood", d, "fill-rule": "evenodd", fill: `url(#${o.id}-wood-${suffix})`, opacity });
59
+ // The boards are meant to be seen only when looked for: the tones and the seams are laid at half strength.
60
+ return layer("a", 0.5) + layer("b", 0.45) + layer("s");
61
+ };
46
62
  let pending = false;
47
63
  let wove = false;
48
64
  for (const a of plan.areas) {
@@ -54,6 +70,10 @@ export function budoSurface(o, plan, margin) {
54
70
  if (tone)
55
71
  pending = true;
56
72
  ground += el("path", { class: ft(o, a.feature, a.cls), d: areaD(a), "fill-rule": a.evenodd === true ? "evenodd" : undefined });
73
+ if (full && a.id === "surround" && !polished) {
74
+ polished = true;
75
+ ground += polish(areaD(a));
76
+ }
57
77
  }
58
78
  if (weave && !wove)
59
79
  ground += woven();
@@ -9,4 +9,6 @@ export { CURLING_PRESETS, LONG_PRESETS, NET_PRESETS, PITCH_MAX, STEP, TURN_DEFAU
9
9
  export type { Box3, Mat4, Placement, TurnPreset, TurnToward, TurnView } from "./turn.camera.ts";
10
10
  export { describeUprights, heightUnit, planesOf, uprightsFor } from "./uprights.ts";
11
11
  export type { UprightOptions, Uprights } from "./uprights.ts";
12
+ export { ROOF } from "./venue.ts";
13
+ export type { Venue, VenueBuild, VenueSettings } from "./venue.ts";
12
14
  export type { Plane, PartKind, Upright, UprightKind, V3 } from "./turn.kit.ts";
@@ -5,3 +5,4 @@ export { layoutOf, planeMarkup, planeMatrix, turnMarkup, turnMarkupOf } from "./
5
5
  export { KYOGIJO_TURN_STYLE } from "./turn.style.js";
6
6
  export { CURLING_PRESETS, LONG_PRESETS, NET_PRESETS, PITCH_MAX, STEP, TURN_DEFAULT, TURN_PRESETS, TURN_PRESET_NAMES, ZOOM_MAX, ZOOM_MIN, cameraMatrix, clampView, depthOf, drag, eyeDistance, fit, matrix3d, multiply, nudge, presetOf, presetView, presetsFor, project, step } from "./turn.camera.js";
7
7
  export { describeUprights, heightUnit, planesOf, uprightsFor } from "./uprights.js";
8
+ export { ROOF } from "./venue.js";
@@ -15,6 +15,14 @@ export interface KyogijoTurn {
15
15
  reset(): void;
16
16
  /** Draw again with new options (a score that moved, a look that changed), keeping the view. */
17
17
  update(options: SurfaceOptions | ScoreOptions): void;
18
+ /** Whether the auto-turn is running. */
19
+ readonly playing: boolean;
20
+ /** Start the auto-turn, a slow turn of the whole view. It gives whether it is running (a surface that cannot turn gives `false`). Nothing turns by itself until this, the Play button or Space starts it, unless `autoPlay` asked. */
21
+ play(): boolean;
22
+ /** Stop the auto-turn. */
23
+ pause(): void;
24
+ /** Start the auto-turn if it is stopped and stop it if it is running; it gives whether it is running now. */
25
+ togglePlay(): boolean;
18
26
  /** The flat drawing that is the floor, or `null` after `destroy`. */
19
27
  svg(): SVGSVGElement | null;
20
28
  /** Stop listening and empty the container. */
package/dist/turn.dom.js CHANGED
@@ -15,7 +15,7 @@ export function turnSupported(win = typeof window === "undefined" ? undefined :
15
15
  return false;
16
16
  return css.supports("transform", "matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1)");
17
17
  }
18
- const KEYS = { ArrowLeft: true, ArrowRight: true, ArrowUp: true, ArrowDown: true, "+": true, "=": true, "-": true, _: true, "0": true, Home: true, "1": true, "2": true, "3": true, "4": true };
18
+ const KEYS = { " ": true, ArrowLeft: true, ArrowRight: true, ArrowUp: true, ArrowDown: true, "+": true, "=": true, "-": true, _: true, "0": true, Home: true, "1": true, "2": true, "3": true, "4": true };
19
19
  /** The view part of the way from one to another, `t` from 0 to 1, the turn taken the short way round. */
20
20
  const between = (from, to, t) => ({ yaw: from.yaw + (nearest(from.yaw, to.yaw) - from.yaw) * t, pitch: from.pitch + (to.pitch - from.pitch) * t, zoom: from.zoom * Math.pow(to.zoom / from.zoom, t) });
21
21
  /**
@@ -48,6 +48,10 @@ export function mountKyogijoTurn(host, options, settings = {}) {
48
48
  set: () => undefined,
49
49
  preset: () => undefined,
50
50
  reset: () => undefined,
51
+ playing: false,
52
+ play: () => false,
53
+ pause: () => undefined,
54
+ togglePlay: () => false,
51
55
  update: (next) => {
52
56
  model = buildTurn(next, settings);
53
57
  flat(next);
@@ -70,6 +74,12 @@ export function mountKyogijoTurn(host, options, settings = {}) {
70
74
  let opening = model.view;
71
75
  let flight;
72
76
  let spoken;
77
+ // The auto-turn: whether it is on, the frame that will advance it, the time of the last one, and whether it has moved the view since the page was last told.
78
+ let playing = false;
79
+ let tickFrame;
80
+ let lastTick;
81
+ let advanced = false;
82
+ const speed = typeof settings.speed === "number" && Number.isFinite(settings.speed) ? settings.speed : 12;
73
83
  const calm = () => settings.motion === "none" || (settings.motion !== "smooth" && media?.matches === true);
74
84
  // The elements, floor first, and what was written to each, so that only a change is written.
75
85
  let elements = [];
@@ -84,6 +94,18 @@ export function mountKyogijoTurn(host, options, settings = {}) {
84
94
  };
85
95
  gather();
86
96
  const buttons = () => root.querySelectorAll("button[data-preset]");
97
+ /** The Play button says what it is and what it does: pressed while the view turns. */
98
+ const syncPlay = () => {
99
+ const button = root.querySelector("button[data-act=play]");
100
+ if (button === null)
101
+ return;
102
+ const words = turnWords(model);
103
+ button.setAttribute("aria-pressed", String(playing));
104
+ button.title = playing ? words.playOff : words.playOn;
105
+ const glyph = button.querySelector("[data-glyph]");
106
+ if (glyph !== null)
107
+ glyph.textContent = playing ? "❚❚" : "▶";
108
+ };
87
109
  /** Put a view on the screen. */
88
110
  const paint = (now) => {
89
111
  const width = stage.clientWidth || 960;
@@ -155,7 +177,10 @@ export function mountKyogijoTurn(host, options, settings = {}) {
155
177
  const angles = () => turnWords(model).announceAngle.replace("{yaw}", String(Math.round(((view.yaw % 360) + 360) % 360))).replace("{pitch}", String(Math.round(90 - view.pitch)));
156
178
  const changed = () => settings.onChange?.(view, presetOf(view, sport()));
157
179
  /** Move to a view the controls asked for: glide, say where it is, tell the page. */
158
- const go = (next, ms, speak = "angle") => {
180
+ const go = (next, ms, speak = "angle", keepPlaying = false) => {
181
+ // Anything the reader (or the page) asks of the view takes it over from the auto-turn, but a zoom, which does not turn it.
182
+ if (!keepPlaying)
183
+ setPlaying(false, true);
159
184
  // A turn takes the short way round, even from 170 degrees to -170.
160
185
  show(clampView({ ...next, yaw: nearest(view.yaw, next.yaw) }), ms);
161
186
  if (speak === "preset") {
@@ -172,6 +197,82 @@ export function mountKyogijoTurn(host, options, settings = {}) {
172
197
  go(opening, 560, "none");
173
198
  say(turnWords(model).reset);
174
199
  };
200
+ // ---- The auto-turn: a trophy case's slow turn. It runs only when asked, holds while the pointer holds the view, and does nothing while the page is hidden. ----
201
+ const hidden = () => doc.hidden === true;
202
+ const stopTick = () => {
203
+ if (tickFrame !== undefined)
204
+ win?.cancelAnimationFrame(tickFrame);
205
+ tickFrame = undefined;
206
+ lastTick = undefined;
207
+ };
208
+ const tick = (stamp) => {
209
+ tickFrame = undefined;
210
+ if (!playing || !alive)
211
+ return;
212
+ // Hidden, off the screen or held: the loop ends here, and the visibility change, the scroll back or the pointer's release starts it again.
213
+ if (hidden() || !onScreen || down.size > 0) {
214
+ lastTick = undefined;
215
+ return;
216
+ }
217
+ // About thirty frames a second are plenty for a slow turn, and every plane is written at each.
218
+ if (lastTick !== undefined && stamp - lastTick < 30) {
219
+ loop();
220
+ return;
221
+ }
222
+ const dt = lastTick === undefined ? 0 : Math.min(0.1, (stamp - lastTick) / 1000);
223
+ lastTick = stamp;
224
+ if (dt > 0) {
225
+ stop();
226
+ view = clampView({ ...view, yaw: view.yaw - speed * dt });
227
+ shown = view;
228
+ advanced = true;
229
+ paint(shown);
230
+ label();
231
+ }
232
+ loop();
233
+ };
234
+ const loop = () => {
235
+ if (tickFrame === undefined && playing && alive && win !== null)
236
+ tickFrame = win.requestAnimationFrame(tick);
237
+ };
238
+ /** Start or stop the auto-turn. `quiet` leaves the live region alone (a key or a drag stopped it, and says its own thing). */
239
+ const setPlaying = (next, quiet = false) => {
240
+ if (playing === next)
241
+ return;
242
+ playing = next;
243
+ stopTick();
244
+ syncPlay();
245
+ if (next)
246
+ loop();
247
+ else if (advanced) {
248
+ // The page is told where the view ended up, once, not at every frame.
249
+ advanced = false;
250
+ changed();
251
+ }
252
+ if (!quiet)
253
+ say(next ? turnWords(model).announcePlay : turnWords(model).announcePause);
254
+ settings.onPlayChange?.(next);
255
+ };
256
+ // A view scrolled out of sight does not turn.
257
+ let onScreen = true;
258
+ let seen;
259
+ if (typeof IntersectionObserver === "function") {
260
+ seen = new IntersectionObserver((entries) => {
261
+ onScreen = entries[entries.length - 1]?.isIntersecting !== false;
262
+ if (onScreen && playing) {
263
+ lastTick = undefined;
264
+ loop();
265
+ }
266
+ });
267
+ seen.observe(stage);
268
+ }
269
+ const onVisible = () => {
270
+ if (playing && !hidden()) {
271
+ lastTick = undefined;
272
+ loop();
273
+ }
274
+ };
275
+ doc.addEventListener("visibilitychange", onVisible);
175
276
  // ---- Buttons. ----
176
277
  const onClick = (event) => {
177
278
  const target = event.target?.closest("button");
@@ -187,8 +288,9 @@ export function mountKyogijoTurn(host, options, settings = {}) {
187
288
  case "turn-right": return go(nudge(view, "right", big ? STEP.yawBig : STEP.yaw * 2), 180);
188
289
  case "raise": return go(nudge(view, "up", big ? STEP.pitchBig : STEP.pitch * 2), 180);
189
290
  case "lower": return go(nudge(view, "down", big ? STEP.pitchBig : STEP.pitch * 2), 180);
190
- case "zoom-in": return go(step(view, { zoom: STEP.zoom }), 180, "none");
191
- case "zoom-out": return go(step(view, { zoom: 1 / STEP.zoom }), 180, "none");
291
+ case "zoom-in": return go(step(view, { zoom: STEP.zoom }), 180, "none", true);
292
+ case "zoom-out": return go(step(view, { zoom: 1 / STEP.zoom }), 180, "none", true);
293
+ case "play": return void setPlaying(!playing);
192
294
  case "reset": return toOpening();
193
295
  default:
194
296
  }
@@ -209,9 +311,10 @@ export function mountKyogijoTurn(host, options, settings = {}) {
209
311
  case "ArrowUp": return go(nudge(view, "up", pitch), 120);
210
312
  case "ArrowDown": return go(nudge(view, "down", pitch), 120);
211
313
  case "+":
212
- case "=": return go(step(view, { zoom: STEP.zoom }), 120, "none");
314
+ case "=": return go(step(view, { zoom: STEP.zoom }), 120, "none", true);
213
315
  case "-":
214
- case "_": return go(step(view, { zoom: 1 / STEP.zoom }), 120, "none");
316
+ case "_": return go(step(view, { zoom: 1 / STEP.zoom }), 120, "none", true);
317
+ case " ": return void setPlaying(!playing);
215
318
  case "0":
216
319
  case "Home": return toOpening();
217
320
  default: toPreset(TURN_PRESET_NAMES[Number(event.key) - 1]);
@@ -240,6 +343,8 @@ export function mountKyogijoTurn(host, options, settings = {}) {
240
343
  }
241
344
  moved = false;
242
345
  grab();
346
+ // The auto-turn holds while the pointer does.
347
+ stopTick();
243
348
  if (down.size === 1)
244
349
  root.setAttribute("data-dragging", "");
245
350
  if (down.size === 2) {
@@ -273,6 +378,8 @@ export function mountKyogijoTurn(host, options, settings = {}) {
273
378
  if (down.size === 0) {
274
379
  root.removeAttribute("data-dragging");
275
380
  label();
381
+ if (playing)
382
+ loop();
276
383
  if (moved) {
277
384
  say(angles(), 450);
278
385
  changed();
@@ -309,6 +416,9 @@ export function mountKyogijoTurn(host, options, settings = {}) {
309
416
  observer = new ResizeObserver(() => alive && paint(shown));
310
417
  observer.observe(stage);
311
418
  }
419
+ // Nothing moves until the reader asks, unless the page did: `autoPlay` starts it, but not for a reader who prefers reduced motion (`"force"` does).
420
+ if (settings.autoPlay === "force" || (settings.autoPlay === true && !calm()))
421
+ setPlaying(true, true);
312
422
  return {
313
423
  mode: "3d",
314
424
  view: () => ({ ...view }),
@@ -324,7 +434,19 @@ export function mountKyogijoTurn(host, options, settings = {}) {
324
434
  view = clampView(view);
325
435
  opening = openingView(settings, sport());
326
436
  const layout = layoutOf(model, shown, stage.clientWidth || 960, stage.clientHeight || 600, settings.lens);
327
- root.querySelector(".ky-turn-world").innerHTML = worldMarkup(model, layout);
437
+ const world = root.querySelector(".ky-turn-world");
438
+ world.innerHTML = worldMarkup(model, layout);
439
+ // The look the floor is painted in (and the theme) are attributes of the world.
440
+ for (const name of ["data-sport", "data-look", "data-theme"])
441
+ world.removeAttribute(name);
442
+ world.className = model.look === undefined ? "ky-turn-world" : "ky-turn-world kyogijo";
443
+ if (model.look !== undefined) {
444
+ world.setAttribute("data-sport", model.look.sport);
445
+ world.setAttribute("data-look", model.look.look);
446
+ const forced = model.options.theme;
447
+ if (forced === "light" || forced === "dark")
448
+ world.setAttribute("data-theme", forced);
449
+ }
328
450
  gather();
329
451
  stage.setAttribute("aria-label", model.title);
330
452
  root.querySelector(".ky-turn-help").textContent = `${turnWords(model).keys}${model.standing === "" ? "" : ` ${model.standing}`}`;
@@ -335,16 +457,33 @@ export function mountKyogijoTurn(host, options, settings = {}) {
335
457
  root.removeAttribute("data-theme");
336
458
  const controls = root.querySelector(".ky-turn-controls");
337
459
  if (controls !== null)
338
- controls.outerHTML = controlsMarkup(model, view);
460
+ controls.outerHTML = controlsMarkup(model, view, playing);
339
461
  paint(shown);
340
462
  label();
341
463
  },
464
+ get playing() {
465
+ return playing;
466
+ },
467
+ play() {
468
+ setPlaying(true);
469
+ return playing;
470
+ },
471
+ pause() {
472
+ setPlaying(false);
473
+ },
474
+ togglePlay() {
475
+ setPlaying(!playing);
476
+ return playing;
477
+ },
342
478
  svg: () => (alive ? root.querySelector(".ky-turn-floor svg.kyogijo") : null),
343
479
  destroy() {
344
480
  if (!alive)
345
481
  return;
346
482
  alive = false;
347
483
  stop();
484
+ stopTick();
485
+ seen?.disconnect();
486
+ doc.removeEventListener("visibilitychange", onVisible);
348
487
  clearTimeout(spoken);
349
488
  observer?.disconnect();
350
489
  host.replaceChildren();
@@ -1,7 +1,7 @@
1
1
  /** A point or a direction: x, y, z in feet. */
2
2
  export type V3 = readonly [number, number, number];
3
3
  /** What a plane is a part of: it is the class of its element (`ky-up-<kind>`) and what a test finds it by. */
4
- export type PartKind = "post" | "net" | "tape" | "antenna" | "board" | "glass" | "rim" | "stanchion" | "frame" | "goal" | "flag" | "stump" | "leg" | "table" | "pylon" | "wall" | "screen" | "pole" | "ground" | "target";
4
+ export type PartKind = "post" | "net" | "tape" | "antenna" | "board" | "glass" | "rim" | "stanchion" | "frame" | "goal" | "flag" | "stump" | "leg" | "table" | "pylon" | "wall" | "screen" | "pole" | "ground" | "target" | "seats" | "concourse" | "fascia" | "roof" | "banner" | "platform" | "rail" | "matedge" | "shrine";
5
5
  /** One flat rectangle. Its own x runs along `u` (its whole length) and its own y along `v`; `o` is the corner both start from. `svg` is what is drawn on it, in feet along `u` and `v`. */
6
6
  export interface Plane {
7
7
  /** A name that says what it is and where: `net`, `post-left`, `goal-right-back`. */
@@ -21,9 +21,11 @@ export interface Plane {
21
21
  * wall. The nearer the eye is to that side, and the lower it is, the more the wall is cut away (see `fadeOf`), so that the court is never hidden behind its own glass.
22
22
  */
23
23
  shy?: readonly [number, number];
24
+ /** How much of a `shy` plane is left when it is cut away altogether: 0.08 when missing. A plane that others lie over on the screen (the steps of a gallery) takes less, so that they do not add up to a veil. */
25
+ fadeTo?: number;
24
26
  }
25
27
  /** What stands on the surface as a thing a reader would name: a net, a basket, a goal, a pair of posts. */
26
- export type UprightKind = "scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets";
28
+ export type UprightKind = "scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets" | "stands" | "wall-seats" | "roof" | "gallery" | "dais" | "banner" | "shrine" | "mat-edge";
27
29
  /** A thing that stands on the surface: where, how high, and the planes it is made of. */
28
30
  export interface Upright {
29
31
  id: string;
@@ -83,4 +85,4 @@ export declare function shy(p: Plane, centre: readonly [number, number]): Plane;
83
85
  * How much of a wall is left (1 whole, down to 0.08) at a view: none is cut away unless the wall is on the eye's side of the surface, and then more the lower the eye is, because a wall seen
84
86
  * from straight above hides nothing. `yaw` and `pitch` are the view's, in degrees.
85
87
  */
86
- export declare function fadeOf(p: Pick<Plane, "shy">, yaw: number, pitch: number): number;
88
+ export declare function fadeOf(p: Pick<Plane, "shy" | "fadeTo">, yaw: number, pitch: number): number;
package/dist/turn.kit.js CHANGED
@@ -106,5 +106,5 @@ export function fadeOf(p, yaw, pitch) {
106
106
  const toward = p.shy[0] * Math.sin(a) + p.shy[1] * Math.cos(a);
107
107
  const hides = toward * Math.sin((pitch * Math.PI) / 180);
108
108
  const t = Math.min(1, Math.max(0, (hides - 0.1) / 0.25));
109
- return Math.round((1 - 0.92 * t) * 1000) / 1000;
109
+ return Math.round((1 - (1 - (p.fadeTo ?? 0.08)) * t) * 1000) / 1000;
110
110
  }
@@ -23,10 +23,17 @@ export declare function layoutOf(model: TurnModel, view: TurnView, width: number
23
23
  export declare function planeMarkup(p: Plane, scale: number, transform: string, stack: number, fade?: number): string;
24
24
  /** The floor as an element: the flat drawing, in the pixels of the scene. */
25
25
  export declare function floorMarkup(model: TurnModel, transform: string, stack: number): string;
26
+ /** The patterns the venue's planes are drawn with, once, in a box of no size: every plane's SVG reads them by id, and the crowd's colours are on this element. */
27
+ export declare function defsMarkup(model: TurnModel): string;
28
+ /**
29
+ * The attributes of the element the planes stand in. A floor painted in a look (a dojo's mats) gets the drawing's own class, sport and look, so that the stylesheet's rules for the look set
30
+ * its custom properties here too, and the walls and the wood of the room are the colours of the floor. A theme the options force goes with them.
31
+ */
32
+ export declare function worldAttributes(model: TurnModel): string;
26
33
  /** The floor and the planes of a model, laid out. */
27
34
  export declare function worldMarkup(model: TurnModel, layout: Layout): string;
28
35
  /** The buttons under the view. */
29
- export declare function controlsMarkup(model: TurnModel, view: TurnView): string;
36
+ export declare function controlsMarkup(model: TurnModel, view: TurnView, playing?: boolean): string;
30
37
  /** The size, in pixels, the first view is worked out for. */
31
38
  export declare const sizeOf: (settings: TurnSettings) => {
32
39
  width: number;
@@ -52,12 +52,24 @@ export function floorMarkup(model, transform, stack) {
52
52
  const vb = model.viewBox;
53
53
  return `<div class="ky-turn-floor" style="width:${px(vb.w * model.scale)}px;height:${px(vb.h * model.scale)}px;transform:${transform};z-index:${stack}">${model.floor}</div>`;
54
54
  }
55
+ /** The patterns the venue's planes are drawn with, once, in a box of no size: every plane's SVG reads them by id, and the crowd's colours are on this element. */
56
+ export function defsMarkup(model) {
57
+ return model.defs === "" ? "" : `<svg class="ky-turn-defs" xmlns="http://www.w3.org/2000/svg" width="0" height="0"${model.crowdStyle === "" ? "" : ` style="${esc(model.crowdStyle)}"`} aria-hidden="true" focusable="false"><defs>${model.defs}</defs></svg>`;
58
+ }
59
+ /**
60
+ * The attributes of the element the planes stand in. A floor painted in a look (a dojo's mats) gets the drawing's own class, sport and look, so that the stylesheet's rules for the look set
61
+ * its custom properties here too, and the walls and the wood of the room are the colours of the floor. A theme the options force goes with them.
62
+ */
63
+ export function worldAttributes(model) {
64
+ const theme = model.options.theme;
65
+ return model.look === undefined ? 'class="ky-turn-world"' : `class="ky-turn-world kyogijo" data-sport="${esc(model.look.sport)}" data-look="${esc(model.look.look)}"${theme === "light" || theme === "dark" ? ` data-theme="${theme}"` : ""}`;
66
+ }
55
67
  /** The floor and the planes of a model, laid out. */
56
68
  export function worldMarkup(model, layout) {
57
- return floorMarkup(model, layout.transforms[0], layout.stack[0]) + model.planes.map((p, i) => planeMarkup(p, model.scale, layout.transforms[i + 1], layout.stack[i + 1], layout.fade[i + 1])).join("");
69
+ return defsMarkup(model) + floorMarkup(model, layout.transforms[0], layout.stack[0]) + model.planes.map((p, i) => planeMarkup(p, model.scale, layout.transforms[i + 1], layout.stack[i + 1], layout.fade[i + 1])).join("");
58
70
  }
59
71
  /** The buttons under the view. */
60
- export function controlsMarkup(model, view) {
72
+ export function controlsMarkup(model, view, playing = false) {
61
73
  const words = turnWords(model);
62
74
  const here = presetOf(view, model.options.sport);
63
75
  const presets = TURN_PRESET_NAMES.map((name, i) => `<button type="button" data-preset="${name}" aria-pressed="${here === name}" aria-keyshortcuts="${i + 1}">${esc(words[name])}</button>`).join("");
@@ -70,7 +82,9 @@ export function controlsMarkup(model, view) {
70
82
  ["zoom-out", "−", words.zoomOut],
71
83
  ];
72
84
  const buttons = tools.map(([act, glyph, label]) => `<button type="button" data-act="${act}" aria-label="${esc(label)}" title="${esc(label)}">${glyph}</button>`).join("");
73
- return `<div class="ky-turn-controls"><div class="ky-turn-group" role="group" aria-label="${esc(words.group)}">${presets}</div><div class="ky-turn-group">${buttons}<button type="button" data-act="reset" aria-keyshortcuts="0 Home">${esc(words.reset)}</button></div></div>`;
85
+ // The auto-turn: a toggle named for what it is, pressed while the view turns; its glyph and tooltip say what pressing it does.
86
+ const play = `<button type="button" data-act="play" data-wide aria-pressed="${playing}" aria-keyshortcuts="Space" title="${esc(playing ? words.playOff : words.playOn)}"><span data-glyph aria-hidden="true">${playing ? "❚❚" : "▶"}</span>${esc(words.play)}</button>`;
87
+ return `<div class="ky-turn-controls"><div class="ky-turn-group" role="group" aria-label="${esc(words.group)}">${presets}</div><div class="ky-turn-group">${buttons}<button type="button" data-act="reset" aria-keyshortcuts="0 Home">${esc(words.reset)}</button>${play}</div></div>`;
74
88
  }
75
89
  /** The size, in pixels, the first view is worked out for. */
76
90
  export const sizeOf = (settings) => settings.size ?? { width: 960, height: 600 };
@@ -86,7 +100,7 @@ export function turnMarkupOf(model, settings = {}) {
86
100
  const sport = model.options.sport;
87
101
  return (`<div class="ky-turn" id="${esc(model.id)}" data-sport="${esc(String(sport))}" data-mode="3d" data-touch="${settings.touch === "turn" ? "turn" : "scroll"}" data-preset="${presetOf(view, sport) ?? "custom"}"${theme === "light" || theme === "dark" ? ` data-theme="${theme}"` : ""} lang="${model.language}">` +
88
102
  sheet +
89
- `<div class="ky-turn-view" tabindex="0" role="group" aria-roledescription="${esc(words.region)}" aria-label="${esc(model.title)}" aria-describedby="${esc(model.id)}-help" aria-keyshortcuts="ArrowLeft ArrowRight ArrowUp ArrowDown + - 0 1 2 3 4 Home"><div class="ky-turn-world">${worldMarkup(model, layout)}</div></div>` +
103
+ `<div class="ky-turn-view" tabindex="0" role="group" aria-roledescription="${esc(words.region)}" aria-label="${esc(model.title)}" aria-describedby="${esc(model.id)}-help" aria-keyshortcuts="ArrowLeft ArrowRight ArrowUp ArrowDown + - 0 1 2 3 4 Home Space"><div ${worldAttributes(model)}>${worldMarkup(model, layout)}</div></div>` +
90
104
  `<p class="ky-turn-help" id="${esc(model.id)}-help">${esc(keys)}</p>` +
91
105
  (settings.controls === false ? "" : controlsMarkup(model, view)) +
92
106
  `<div class="ky-turn-live" role="status" aria-live="polite"></div>` +