import {Enum} from "@bokehjs/core/kinds" import type * as p from "@bokehjs/core/properties" import {div, empty, span} from "@bokehjs/core/dom" import {Widget, WidgetView} from "@bokehjs/models/widgets/widget" import {to_string} from "@bokehjs/core/util/pretty" const SVG_STRINGS = { slower: '', first: '', previous: '', reverse: '', pause: '', play: '', next: '', last: '', faster: '', } function press(btn_list: HTMLButtonElement[]): void { btn_list.forEach((btn) => btn.style.borderStyle = "inset") } function unpress(btn_list: HTMLButtonElement[]): void { btn_list.forEach((btn) => btn.style.borderStyle = "outset") } export class PlayerView extends WidgetView { declare model: Player protected buttonEl: HTMLDivElement protected titleEl: HTMLDivElement protected groupEl: HTMLDivElement protected sliderEl: HTMLInputElement protected loop_state: HTMLFormElement protected timer: any protected _toggle_reverse: CallableFunction protected _toogle_pause: CallableFunction protected _toggle_play: CallableFunction protected _changing: boolean = false protected slower: HTMLButtonElement protected first: HTMLButtonElement protected previous: HTMLButtonElement protected reverse: HTMLButtonElement protected pause: HTMLButtonElement protected play: HTMLButtonElement protected next: HTMLButtonElement protected last: HTMLButtonElement protected faster: HTMLButtonElement override connect_signals(): void { super.connect_signals() const {title, value_align, direction, value, loop_policy, disabled, show_loop_controls, show_value, scale_buttons, visible_buttons, visible_loop_options} = this.model.properties this.on_change(title, () => this.update_title_and_value()) this.on_change(value_align, () => this.set_value_align()) this.on_change(direction, () => this.set_direction()) this.on_change(value, () => this.render()) this.on_change(loop_policy, () => this.set_loop_state(this.model.loop_policy)) this.on_change(disabled, () => this.toggle_disable()) this.on_change(show_loop_controls, () => { if (this.model.show_loop_controls && this.loop_state.parentNode != this.groupEl) { this.groupEl.appendChild(this.loop_state) } else if (!this.model.show_loop_controls && this.loop_state.parentNode == this.groupEl) { this.groupEl.removeChild(this.loop_state) } }) this.on_change(show_value, () => this.update_title_and_value()) this.on_change(scale_buttons, () => this.update_css()) this.on_change(visible_buttons, () => this.update_css()) this.on_change(visible_loop_options, () => this.update_css()) } toggle_disable() { this.sliderEl.disabled = this.model.disabled for (const el of this.buttonEl.children) { const anyEl = el as any anyEl.disabled = this.model.disabled } for (const el of this.loop_state.children) { if (el.tagName == "input") { const anyEl = el as any anyEl.disabled = this.model.disabled } } } get_height(): number { return 250 } update_css(): void { const button_style_small = `text-align: center; flex-grow: 1; margin: 2px; transform: scale(${this.model.scale_buttons}); max-width: 50px;` const button_style = `text-align: center; flex-grow: 2; margin: 2px; transform: scale(${this.model.scale_buttons}); max-width: 50px;` const buttons = { slower: this.slower, first: this.first, previous: this.previous, reverse: this.reverse, pause: this.pause, play: this.play, next: this.next, last: this.last, faster: this.faster, } for (const [name, button] of Object.entries(buttons)) { if (button) { if (this.model.visible_buttons.includes(name)) { button.style.display = "" // Reset to default display if (name === "slower" || name === "faster") { button.style.cssText += button_style_small } else { button.style.cssText += button_style } } else { button.style.display = "none" // Hide the button completely } } } for (const el of this.loop_state.children) { if (el.tagName.toLowerCase() == "input") { const anyEl = el as any if (this.model.visible_loop_options.includes(anyEl.value)) { anyEl.style.display = "" } else { anyEl.style.display = "none" } } else if (el.tagName.toLowerCase() == "label") { const anyEl = el as any if (this.model.visible_loop_options.includes(anyEl.innerHTML.toLowerCase())) { anyEl.style.display = "" } else { anyEl.style.display = "none" } } } } override render(): void { if (this.sliderEl == null) { super.render() } else { this.sliderEl.min = String(this.model.start) this.sliderEl.max = String(this.model.end) this.sliderEl.value = String(this.model.value) return } // Layout to group the elements this.groupEl = div() this.groupEl.style.display = "flex" this.groupEl.style.flexDirection = "column" // Display Value this.titleEl = div() this.titleEl.classList.add("pn-player-title") this.titleEl.style.padding = "0 5px 0 5px" this.update_title_and_value() this.set_value_align() // Slider this.sliderEl = document.createElement("input") this.sliderEl.style.width = "100%" this.sliderEl.setAttribute("type", "range") this.sliderEl.value = String(this.model.value) this.sliderEl.min = String(this.model.start) this.sliderEl.max = String(this.model.end) this.sliderEl.addEventListener("input", (ev) => { this.set_frame(parseInt((ev.target as HTMLInputElement).value), false) }) this.sliderEl.addEventListener("change", (ev) => { this.set_frame(parseInt((ev.target as HTMLInputElement).value)) }) // Buttons const button_div = div() as any this.buttonEl = button_div button_div.style.cssText = "margin: 0 auto; display: flex; padding: 5px; align-items: stretch; justify-content: center; width: 100%;" this.slower = document.createElement("button") this.slower.classList.add("slower") this.slower.innerHTML = SVG_STRINGS.slower this.slower.onclick = () => this.slower_speed() button_div.appendChild(this.slower) this.first = document.createElement("button") this.first.classList.add("first") this.first.innerHTML = SVG_STRINGS.first this.first.onclick = () => this.first_frame() button_div.appendChild(this.first) this.previous = document.createElement("button") this.previous.classList.add("previous") this.previous.innerHTML = SVG_STRINGS.previous this.previous.onclick = () => this.previous_frame() button_div.appendChild(this.previous) this.reverse = document.createElement("button") this.reverse.classList.add("reverse") this.reverse.innerHTML = SVG_STRINGS.reverse this.reverse.onclick = () => this.reverse_animation() button_div.appendChild(this.reverse) this.pause = document.createElement("button") this.pause.classList.add("pause") this.pause.innerHTML = SVG_STRINGS.pause this.pause.onclick = () => this.pause_animation() button_div.appendChild(this.pause) this.play = document.createElement("button") this.play.classList.add("play") this.play.innerHTML = SVG_STRINGS.play this.play.onclick = () => this.play_animation() button_div.appendChild(this.play) this.next = document.createElement("button") this.next.classList.add("next") this.next.innerHTML = SVG_STRINGS.next this.next.onclick = () => this.next_frame() button_div.appendChild(this.next) this.last = document.createElement("button") this.last.classList.add("last") this.last.innerHTML = SVG_STRINGS.last this.last.onclick = () => this.last_frame() button_div.appendChild(this.last) this.faster = document.createElement("button") this.faster.classList.add("faster") this.faster.innerHTML = SVG_STRINGS.faster this.faster.onclick = () => this.faster_speed() button_div.appendChild(this.faster) // toggle this._toggle_reverse = () => { unpress([this.pause, this.play]) press([this.reverse]) } this._toogle_pause = () => { unpress([this.reverse, this.play]) press([this.pause]) } this._toggle_play = () => { unpress([this.reverse, this.pause]) press([this.play]) } // Loop control this.loop_state = document.createElement("form") this.loop_state.style.cssText = "margin: 0 auto; display: table" const once = document.createElement("input") once.classList.add("once") once.type = "radio" once.value = "once" once.name = "state" const once_label = document.createElement("label") once_label.innerHTML = "Once" once_label.classList.add("once-label") once_label.style.cssText = "padding: 0 10px 0 5px; user-select:none;" const loop = document.createElement("input") loop.classList.add("loop") loop.setAttribute("type", "radio") loop.setAttribute("value", "loop") loop.setAttribute("name", "state") const loop_label = document.createElement("label") loop_label.classList.add("loop-label") loop_label.innerHTML = "Loop" loop_label.style.cssText = "padding: 0 10px 0 5px; user-select:none;" const reflect = document.createElement("input") reflect.classList.add("reflect") reflect.setAttribute("type", "radio") reflect.setAttribute("value", "reflect") reflect.setAttribute("name", "state") const reflect_label = document.createElement("label") loop_label.classList.add("reflect-label") reflect_label.innerHTML = "Reflect" reflect_label.style.cssText = "padding: 0 10px 0 5px; user-select:none;" if (this.model.loop_policy == "once") { once.checked = true } else if (this.model.loop_policy == "loop") { loop.checked = true } else { reflect.checked = true } // Compose everything this.loop_state.appendChild(once) this.loop_state.appendChild(once_label) this.loop_state.appendChild(loop) this.loop_state.appendChild(loop_label) this.loop_state.appendChild(reflect) this.loop_state.appendChild(reflect_label) this.groupEl.appendChild(this.titleEl) this.groupEl.appendChild(this.sliderEl) this.groupEl.appendChild(button_div) if (this.model.show_loop_controls) { this.groupEl.appendChild(this.loop_state) } this.toggle_disable() this.update_css() this.shadow_el.appendChild(this.groupEl) } set_frame(frame: number, throttled: boolean = true): void { this.model.value = frame this.update_title_and_value() if (throttled) { this.model.value_throttled = frame } if (this.sliderEl.value != String(frame)) { this.sliderEl.value = String(frame) } } get_loop_state(): string { const button_group = this.loop_state.state for (let i = 0; i < button_group.length; i++) { const button = button_group[i] if (button.checked) { return button.value } } return "once" } update_title_and_value(): void { empty(this.titleEl) const hide_header = this.model.title == null || (this.model.title.length == 0 && !this.model.show_value) this.titleEl.style.display = hide_header ? "none" : "" if (!hide_header) { this.titleEl.style.visibility = "visible" const {title} = this.model if (title != null && title.length > 0) { if (this.contains_tex_string(title)) { this.titleEl.innerHTML = `${this.process_tex(title)}` if (this.model.show_value) { this.titleEl.innerHTML += ": " } } else { this.titleEl.textContent = `${title}` if (this.model.show_value) { this.titleEl.textContent += ": " } } } if (this.model.show_value) { this.append_value_to_title_el() } } else { this.titleEl.style.visibility = "hidden" } } append_value_to_title_el(): void { this.titleEl.appendChild(span({class: "pn-player-value"}, to_string(this.model.value))) } set_value_align(): void { switch (this.model.value_align) { case "start": this.titleEl.style.textAlign = "left" break case "center": this.titleEl.style.textAlign = "center" break case "end": this.titleEl.style.textAlign = "right" break } } set_loop_state(state: string): void { const button_group = this.loop_state.state for (let i = 0; i < button_group.length; i++) { const button = button_group[i] if (button.value == state) { button.checked = true } } } next_frame(): void { this.set_frame(Math.min(this.model.end, this.model.value + this.model.step)) } previous_frame(): void { this.set_frame(Math.max(this.model.start, this.model.value - this.model.step)) } first_frame(): void { this.set_frame(this.model.start) } last_frame(): void { this.set_frame(this.model.end) } updateSpeedButton(button: HTMLButtonElement, interval: number, originalSVG: string): void { const fps = 1000 / interval button.innerHTML = `${fps.toFixed(1)}
fps` setTimeout(() => { button.innerHTML = originalSVG }, this.model.preview_duration) // Show for 1.5 seconds } slower_speed(): void { this.model.interval = Math.round(this.model.interval / 0.7) this.updateSpeedButton(this.slower, this.model.interval, SVG_STRINGS.slower) if (this.model.direction > 0) { this.play_animation() } else if (this.model.direction < 0) { this.reverse_animation() } } faster_speed(): void { this.model.interval = Math.round(this.model.interval * 0.7) this.updateSpeedButton(this.faster, this.model.interval, SVG_STRINGS.faster) if (this.model.direction > 0) { this.play_animation() } else if (this.model.direction < 0) { this.reverse_animation() } } anim_step_forward(): void { if (this.model.value < this.model.end) { this.next_frame() } else { const loop_state = this.get_loop_state() if (loop_state == "loop") { this.first_frame() } else if (loop_state == "reflect") { this.last_frame() this.reverse_animation() } else { this.pause_animation() this.last_frame() } } } anim_step_reverse(): void { if (this.model.value > this.model.start) { this.previous_frame() } else { const loop_state = this.get_loop_state() if (loop_state == "loop") { this.last_frame() } else if (loop_state == "reflect") { this.first_frame() this.play_animation() } else { this.pause_animation() this.first_frame() } } } set_direction(): void { if (this._changing) { return } else if (this.model.direction === 0) { this.pause_animation() } else if (this.model.direction === 1) { this.play_animation() } else if (this.model.direction === -1) { this.reverse_animation() } } pause_animation(): void { this._toogle_pause() this._changing = true this.model.direction = 0 this._changing = false if (this.timer) { clearInterval(this.timer) this.timer = null } } play_animation(): void { this.pause_animation() this._toggle_play() this._changing = true this.model.direction = 1 this._changing = false if (!this.timer) { this.timer = setInterval(() => this.anim_step_forward(), this.model.interval) } } reverse_animation(): void { this.pause_animation() this._toggle_reverse() this._changing = true this.model.direction = -1 this._changing = false if (!this.timer) { this.timer = setInterval(() => this.anim_step_reverse(), this.model.interval) } } } export const LoopPolicy = Enum("once", "loop", "reflect") export namespace Player { export type Attrs = p.AttrsOf export type Props = Widget.Props & { direction: p.Property interval: p.Property start: p.Property end: p.Property step: p.Property loop_policy: p.Property title: p.Property value: p.Property value_align: p.Property value_throttled: p.Property preview_duration: p.Property show_loop_controls: p.Property show_value: p.Property button_scale: p.Property scale_buttons: p.Property visible_buttons: p.Property visible_loop_options: p.Property } } export interface Player extends Player.Attrs { } export class Player extends Widget { declare properties: Player.Props constructor(attrs?: Partial) { super(attrs) } static override __module__ = "panel.models.widgets" static { this.prototype.default_view = PlayerView this.define(({Bool, Int, Float, List, Str}) => ({ direction: [Int, 0], interval: [Int, 500], start: [Int, 0], end: [Int, 10], step: [Int, 1], loop_policy: [LoopPolicy, "once"], title: [Str, ""], value: [Int, 0], value_align: [Str, "start"], value_throttled: [Int, 0], preview_duration: [Int, 1500], show_loop_controls: [Bool, true], show_value: [Bool, true], button_scale: [Float, 1], scale_buttons: [Float, 1], visible_buttons: [List(Str), ["slower", "first", "previous", "reverse", "pause", "play", "next", "last", "faster"]], visible_loop_options: [List(Str), ["once", "loop", "reflect"]], })) this.override({width: 400}) } }