diff options
author | metamuffin <metamuffin@disroot.org> | 2024-01-28 01:08:49 +0100 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2024-01-28 01:08:49 +0100 |
commit | b514ec8cea2c2143e0bd7a0eb377c96a6f091d0d (patch) | |
tree | 25b3f8c80e1754b3e9e5d5419d5be276f940b1c1 /web | |
parent | 59ef86b0a637ec3ce44ca495c6d22ddf61649134 (diff) | |
download | jellything-b514ec8cea2c2143e0bd7a0eb377c96a6f091d0d.tar jellything-b514ec8cea2c2143e0bd7a0eb377c96a6f091d0d.tar.bz2 jellything-b514ec8cea2c2143e0bd7a0eb377c96a6f091d0d.tar.zst |
add broken positioning code für vtt
Diffstat (limited to 'web')
-rw-r--r-- | web/script/player/mod.ts | 2 | ||||
-rw-r--r-- | web/script/player/player.ts | 5 | ||||
-rw-r--r-- | web/script/player/profiles.ts | 2 | ||||
-rw-r--r-- | web/script/player/track.ts | 207 | ||||
-rw-r--r-- | web/script/player/track/create.ts | 12 | ||||
-rw-r--r-- | web/script/player/track/mod.ts | 24 | ||||
-rw-r--r-- | web/script/player/track/mse.ts | 140 | ||||
-rw-r--r-- | web/script/player/track/vtt.ts | 80 | ||||
-rw-r--r-- | web/style/player.css | 95 |
9 files changed, 291 insertions, 276 deletions
diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts index c4f4473..3d7cc66 100644 --- a/web/script/player/mod.ts +++ b/web/script/player/mod.ts @@ -12,7 +12,7 @@ import { TrackKind, get_track_kind } from "./mediacaps.ts"; import { Player } from "./player.ts"; import { Popup } from "./popup.ts"; import { Playersync } from "./sync.ts" -import { MSEPlayerTrack } from "./track.ts"; +import { MSEPlayerTrack } from "./track/mse.ts"; globalThis.addEventListener("DOMContentLoaded", () => { if (document.body.classList.contains("player")) { diff --git a/web/script/player/player.ts b/web/script/player/player.ts index c5ceed0..6589772 100644 --- a/web/script/player/player.ts +++ b/web/script/player/player.ts @@ -7,10 +7,11 @@ import { OVar, e } from "../jshelper/mod.ts"; import { NodePublic, NodeUserData, SourceTrack, TimeRange } from "./jhls.d.ts"; import { SegmentDownloader } from "./download.ts"; -import { PlayerTrack } from "./track.ts"; +import { PlayerTrack } from "./track/mod.ts"; import { Logger } from "../jshelper/src/log.ts"; import { WatchedState } from "./jhls.d.ts"; import { get_track_kind } from "./mediacaps.ts"; +import { create_track } from "./track/create.ts"; export interface BufferRange extends TimeRange { status: "buffered" | "loading" | "queued" } export class Player { @@ -139,7 +140,7 @@ export class Player { const [track] = this.active_tracks.value.splice(active_index, 1) track.abort.abort() } else if (state && active_index == -1) { - this.active_tracks.value.push((await PlayerTrack.new(this, this.node_id, index, this.tracks![index]))!) + this.active_tracks.value.push((await create_track(this, this.node_id, index, this.tracks![index]))!) if (update) await this.update() } this.active_tracks.change() diff --git a/web/script/player/profiles.ts b/web/script/player/profiles.ts index fe0e64d..b23fbaf 100644 --- a/web/script/player/profiles.ts +++ b/web/script/player/profiles.ts @@ -8,7 +8,7 @@ import { OVar } from "../jshelper/mod.ts"; import { EncodingProfile } from "./jhls.d.ts"; import { profile_to_partial_track, test_media_capability } from "./mediacaps.ts"; import { Player } from "./player.ts"; -import { MSEPlayerTrack } from "./track.ts"; +import { MSEPlayerTrack } from "./track/mse.ts"; const PROFILE_UP_FAC = 0.6 const PROFILE_DOWN_FAC = 0.8 diff --git a/web/script/player/track.ts b/web/script/player/track.ts deleted file mode 100644 index b6a280f..0000000 --- a/web/script/player/track.ts +++ /dev/null @@ -1,207 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2024 metamuffin <metamuffin.org> -*/ -/// <reference lib="dom" /> -import { JhlsTrackIndex, SourceTrack, TimeRange } from "./jhls.d.ts"; -import { OVar } from "../jshelper/mod.ts"; -import { profile_to_partial_track, track_to_content_type } from "./mediacaps.ts"; -import { BufferRange, Player } from "./player.ts"; -import { EncodingProfileExt, ProfileSelector } from "./profiles.ts"; -import { JvttCue } from "./jhls.d.ts"; -import { get_track_kind } from "./mediacaps.ts"; - -export const TARGET_BUFFER_DURATION = 10 -export const MIN_BUFFER_DURATION = 1 - -export interface AppendRange extends TimeRange { buf: ArrayBuffer, index: number, cb: () => void } - -export abstract class PlayerTrack { - public static async new(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<PlayerTrack | undefined> { - const kind = get_track_kind(metadata.kind) - if (kind == "subtitles") return await VttPlayerTrack.new(player, node_id, track_index, metadata) - else return await MSEPlayerTrack.new(player, node_id, track_index, metadata) - } - - constructor( - public track_index: number, - ) { } - public buffered = new OVar<BufferRange[]>([]); - public abort = new AbortController() - async update(_target: number) { } -} - -export class VttPlayerTrack extends PlayerTrack { - private track: TextTrack - - public static async new(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<VttPlayerTrack | undefined> { - try { - const res = await fetch(`/n/${encodeURIComponent(player.node_id)}/stream?format=jvtt&tracks=${track_index}`, { headers: { "Accept": "application/json" } }) - if (!res.ok) return player.error.value = "Cannot download index.", undefined - let index!: JvttCue[] & { error: string } - try { index = await res.json() } - catch (_) { player.set_pers("Error: Failed to fetch node") } - if (index.error) return player.set_pers("server error: " + index.error), undefined - - const t = new VttPlayerTrack(player, node_id, track_index, metadata, index) - return t - } catch (e) { - if (e instanceof TypeError) { - player.set_pers("Cannot download subtitles: Network Error") - } else throw e - } - } - constructor( - private player: Player, - private node_id: string, - track_index: number, - private metadata: SourceTrack, - public cues: JvttCue[], - ) { - super(track_index) - this.track = this.player.video.addTextTrack("subtitles", metadata.name, metadata.language) - for (const cue of cues) { - const c = new VTTCue(cue.start, cue.end, cue.content); - c.line = -2; - this.track.addCue(c) - } - this.track.mode = "showing" - this.abort.signal.addEventListener("abort", () => { - // TODO disable subtitles properly - this.track.mode = "hidden" - }) - } -} - -export class MSEPlayerTrack extends PlayerTrack { - public source_buffer!: SourceBuffer; - private current_load?: AppendRange; - private loading = new Set<number>(); - private append_queue: AppendRange[] = []; - public profile_selector: ProfileSelector - public profile = new OVar<EncodingProfileExt | undefined>(undefined); - - public static async new(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<MSEPlayerTrack | undefined> { - try { - const res = await fetch(`/n/${encodeURIComponent(player.node_id)}/stream?format=jhlsi&tracks=${track_index}`, { headers: { "Accept": "application/json" } }) - if (!res.ok) return player.error.value = "Cannot download index.", undefined - let index!: JhlsTrackIndex & { error: string } - try { index = await res.json() } - catch (_) { player.set_pers("Error: Failed to fetch node") } - if (index.error) return player.set_pers("server error: " + index.error), undefined - - const t = new MSEPlayerTrack(player, node_id, track_index, metadata, index) - await t.init() - return t - } catch (e) { - if (e instanceof TypeError) { - player.set_pers("Cannot download index: Network Error") - } else throw e - } - } - constructor( - private player: Player, - private node_id: string, - track_index: number, - private metadata: SourceTrack, - public index: JhlsTrackIndex, - ) { - super(track_index) - this.profile_selector = new ProfileSelector(player, this, player.downloader.bandwidth) - } - async init() { - await this.profile_selector.select_optimal_profile(this.track_index, this.profile); - const ct = track_to_content_type(this.track_from_profile())! - console.log(`track ${this.track_index} source buffer content-type: ${ct}`); - this.source_buffer = this.player.media_source.addSourceBuffer(ct); - this.abort.signal.addEventListener("abort", () => { - console.log(`destroy source buffer for track ${this.track_index}`); - this.player.media_source.removeSourceBuffer(this.source_buffer) - }) - this.source_buffer.mode = "segments"; - this.source_buffer.addEventListener("updateend", () => { - if (this.abort.signal.aborted) return - if (this.current_load) { - this.current_load.cb(); - this.loading.delete(this.current_load.index); - this.current_load = undefined; - } - this.update_buf_ranges(); - this.tick_append(); - }); - this.source_buffer.addEventListener("error", e => { - console.error("sourcebuffer error", e); - }); - this.source_buffer.addEventListener("abort", e => { - console.error("sourcebuffer abort", e); - }); - } - track_from_profile(): SourceTrack { - if (this.profile.value) return profile_to_partial_track(this.profile.value) - else return this.metadata - } - - update_buf_ranges() { - const ranges: BufferRange[] = []; - for (let i = 0; i < this.source_buffer.buffered.length; i++) { - ranges.push({ - start: this.source_buffer.buffered.start(i), - end: this.source_buffer.buffered.end(i), - status: "buffered" - }); - } - for (const r of this.loading) { - ranges.push({ ...this.index.segments[r], status: "loading" }); - } - this.buffered.value = ranges; - } - - async update(target: number) { - this.update_buf_ranges(); // TODO required? - - const blocking = []; - for (let i = 0; i < this.index.segments.length; i++) { - const seg = this.index.segments[i]; - if (seg.end < target) continue; - if (seg.start >= target + TARGET_BUFFER_DURATION) break; - if (!this.check_buf_collision(seg.start, seg.end)) continue; - if (seg.start <= target + MIN_BUFFER_DURATION) - blocking.push(this.load(i)); - else - this.load(i); - } - await Promise.all(blocking); - } - check_buf_collision(start: number, end: number) { - const EPSILON = 0.01; - for (const r of this.buffered.value) - if (r.end - EPSILON > start && r.start < end - EPSILON) - return false; - return true; - } - - async load(index: number) { - this.loading.add(index); - await this.profile_selector.select_optimal_profile(this.track_index, this.profile); - const url = `/n/${encodeURIComponent(this.node_id)}/stream?format=snippet&webm=true&tracks=${this.track_index}&index=${index}${this.profile.value ? `&profile=${this.profile.value.id}` : ""}`; - const buf = await this.player.downloader.download(url); - await new Promise<void>(cb => { - if (this.abort.signal.aborted) return - this.append_queue.push({ buf, ...this.index.segments[index], index, cb }); - this.tick_append(); - }); - } - tick_append() { - if (this.source_buffer.updating) return; - if (this.append_queue.length) { - const seg = this.append_queue[0]; - this.append_queue.splice(0, 1); - this.current_load = seg; - // TODO why is appending so unreliable?! sometimes it does not add it - this.source_buffer.changeType(track_to_content_type(this.track_from_profile())!); - this.source_buffer.timestampOffset = seg.start; - this.source_buffer.appendBuffer(seg.buf); - } - } -} diff --git a/web/script/player/track/create.ts b/web/script/player/track/create.ts new file mode 100644 index 0000000..f674c3a --- /dev/null +++ b/web/script/player/track/create.ts @@ -0,0 +1,12 @@ +import { get_track_kind } from "../mediacaps.ts"; +import { create_vtt_track } from "./vtt.ts"; +import { create_mse_track } from "./mse.ts"; +import { Player } from "../player.ts"; +import { SourceTrack } from "../jhls.d.ts"; +import { PlayerTrack } from "./mod.ts"; + +export async function create_track(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<PlayerTrack | undefined> { + const kind = get_track_kind(metadata.kind) + if (kind == "subtitles") return await create_vtt_track(player, node_id, track_index, metadata) + else return await create_mse_track(player, node_id, track_index, metadata) +} diff --git a/web/script/player/track/mod.ts b/web/script/player/track/mod.ts new file mode 100644 index 0000000..cdb07cc --- /dev/null +++ b/web/script/player/track/mod.ts @@ -0,0 +1,24 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2024 metamuffin <metamuffin.org> +*/ +/// <reference lib="dom" /> +import { TimeRange } from "../jhls.d.ts"; +import { OVar } from "../../jshelper/mod.ts"; +import { BufferRange } from "../player.ts"; + +export const TARGET_BUFFER_DURATION = 10 +export const MIN_BUFFER_DURATION = 1 + +export interface AppendRange extends TimeRange { buf: ArrayBuffer, index: number, cb: () => void } + +export abstract class PlayerTrack { + constructor( + public track_index: number, + ) { } + public buffered = new OVar<BufferRange[]>([]); + public abort = new AbortController() + async update(_target: number) { } +} + diff --git a/web/script/player/track/mse.ts b/web/script/player/track/mse.ts new file mode 100644 index 0000000..623e149 --- /dev/null +++ b/web/script/player/track/mse.ts @@ -0,0 +1,140 @@ +import { JhlsTrackIndex, SourceTrack } from "../jhls.d.ts"; +import { OVar } from "../../jshelper/mod.ts"; +import { profile_to_partial_track, track_to_content_type } from "../mediacaps.ts"; +import { BufferRange, Player } from "../player.ts"; +import { EncodingProfileExt, ProfileSelector } from "../profiles.ts"; +import { PlayerTrack, AppendRange, TARGET_BUFFER_DURATION, MIN_BUFFER_DURATION } from "./mod.ts"; + +export async function create_mse_track(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<MSEPlayerTrack | undefined> { + try { + const res = await fetch(`/n/${encodeURIComponent(player.node_id)}/stream?format=jhlsi&tracks=${track_index}`, { headers: { "Accept": "application/json" } }); + if (!res.ok) return player.error.value = "Cannot download index.", undefined; + let index!: JhlsTrackIndex & { error: string; }; + try { index = await res.json(); } + catch (_) { player.set_pers("Error: Failed to fetch node"); } + if (index.error) return player.set_pers("server error: " + index.error), undefined; + + const t = new MSEPlayerTrack(player, node_id, track_index, metadata, index); + await t.init(); + return t; + } catch (e) { + if (e instanceof TypeError) { + player.set_pers("Cannot download index: Network Error"); + } else throw e; + } +} + +export class MSEPlayerTrack extends PlayerTrack { + public source_buffer!: SourceBuffer; + private current_load?: AppendRange; + private loading = new Set<number>(); + private append_queue: AppendRange[] = []; + public profile_selector: ProfileSelector; + public profile = new OVar<EncodingProfileExt | undefined>(undefined); + + constructor( + private player: Player, + private node_id: string, + track_index: number, + private metadata: SourceTrack, + public index: JhlsTrackIndex + ) { + super(track_index); + this.profile_selector = new ProfileSelector(player, this, player.downloader.bandwidth); + } + async init() { + await this.profile_selector.select_optimal_profile(this.track_index, this.profile); + const ct = track_to_content_type(this.track_from_profile())!; + console.log(`track ${this.track_index} source buffer content-type: ${ct}`); + this.source_buffer = this.player.media_source.addSourceBuffer(ct); + this.abort.signal.addEventListener("abort", () => { + console.log(`destroy source buffer for track ${this.track_index}`); + this.player.media_source.removeSourceBuffer(this.source_buffer); + }); + this.source_buffer.mode = "segments"; + this.source_buffer.addEventListener("updateend", () => { + if (this.abort.signal.aborted) return; + if (this.current_load) { + this.current_load.cb(); + this.loading.delete(this.current_load.index); + this.current_load = undefined; + } + this.update_buf_ranges(); + this.tick_append(); + }); + this.source_buffer.addEventListener("error", e => { + console.error("sourcebuffer error", e); + }); + this.source_buffer.addEventListener("abort", e => { + console.error("sourcebuffer abort", e); + }); + } + track_from_profile(): SourceTrack { + if (this.profile.value) return profile_to_partial_track(this.profile.value); + else return this.metadata; + } + + update_buf_ranges() { + const ranges: BufferRange[] = []; + for (let i = 0; i < this.source_buffer.buffered.length; i++) { + ranges.push({ + start: this.source_buffer.buffered.start(i), + end: this.source_buffer.buffered.end(i), + status: "buffered" + }); + } + for (const r of this.loading) { + ranges.push({ ...this.index.segments[r], status: "loading" }); + } + this.buffered.value = ranges; + } + + async update(target: number) { + this.update_buf_ranges(); // TODO required? + + const blocking = []; + for (let i = 0; i < this.index.segments.length; i++) { + const seg = this.index.segments[i]; + if (seg.end < target) continue; + if (seg.start >= target + TARGET_BUFFER_DURATION) break; + if (!this.check_buf_collision(seg.start, seg.end)) continue; + if (seg.start <= target + MIN_BUFFER_DURATION) + blocking.push(this.load(i)); + + else + this.load(i); + } + await Promise.all(blocking); + } + check_buf_collision(start: number, end: number) { + const EPSILON = 0.01; + for (const r of this.buffered.value) + if (r.end - EPSILON > start && r.start < end - EPSILON) + return false; + return true; + } + + async load(index: number) { + this.loading.add(index); + await this.profile_selector.select_optimal_profile(this.track_index, this.profile); + const url = `/n/${encodeURIComponent(this.node_id)}/stream?format=snippet&webm=true&tracks=${this.track_index}&index=${index}${this.profile.value ? `&profile=${this.profile.value.id}` : ""}`; + const buf = await this.player.downloader.download(url); + await new Promise<void>(cb => { + if (this.abort.signal.aborted) return; + this.append_queue.push({ buf, ...this.index.segments[index], index, cb }); + this.tick_append(); + }); + } + tick_append() { + if (this.source_buffer.updating) return; + if (this.append_queue.length) { + const seg = this.append_queue[0]; + this.append_queue.splice(0, 1); + this.current_load = seg; + // TODO why is appending so unreliable?! sometimes it does not add it + this.source_buffer.changeType(track_to_content_type(this.track_from_profile())!); + this.source_buffer.timestampOffset = seg.start; + this.source_buffer.appendBuffer(seg.buf); + } + } +} diff --git a/web/script/player/track/vtt.ts b/web/script/player/track/vtt.ts new file mode 100644 index 0000000..c426fa1 --- /dev/null +++ b/web/script/player/track/vtt.ts @@ -0,0 +1,80 @@ +import { SourceTrack, JvttCue } from "../jhls.d.ts"; +import { Player } from "../player.ts"; +import { PlayerTrack } from "./mod.ts"; + +export async function create_vtt_track(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<VttPlayerTrack | undefined> { + let index: JvttCue[]; + try { + const res = await fetch(`/n/${encodeURIComponent(player.node_id)}/stream?format=jvtt&tracks=${track_index}`, { headers: { "Accept": "application/json" } }); + if (!res.ok) return player.error.value = "Cannot download index.", undefined; + let ai!: JvttCue[] & { error: string; }; + try { ai = await res.json(); } + catch (_) { player.set_pers("Error: Failed to fetch node"); } + if (ai.error) return player.set_pers("server error: " + ai.error), undefined; + index = ai; + } catch (e) { + if (e instanceof TypeError) { + player.set_pers("Cannot download subtitles: Network Error"); + return undefined + } else throw e; + } + const t = new VttPlayerTrack(player, node_id, track_index, metadata, index); + return t; +} + +export class VttPlayerTrack extends PlayerTrack { + private track: TextTrack; + + constructor( + private player: Player, + private node_id: string, + track_index: number, + private metadata: SourceTrack, + public cues: JvttCue[] + ) { + super(track_index); + this.track = this.player.video.addTextTrack("subtitles", metadata.name, metadata.language); + for (const cue of cues) { + this.track.addCue(create_cue(cue)); + } + this.track.mode = "showing"; + this.abort.signal.addEventListener("abort", () => { + // TODO disable subtitles properly + this.track.mode = "hidden"; + }); + } +} + +function create_cue(cue: JvttCue): VTTCue { + const c = new VTTCue(cue.start, cue.end, cue.content); + const props = parse_layout_properties(cue.content.split("\n")[0]) + if (props) { + c.text = cue.content.split("\n").slice(1).join("\n") + // TODO this does not work at all... + const region = new VTTRegion() + if ("position" in props && props.position.endsWith("%")) + region.regionAnchorX = parseFloat(props.position.replace("%", "")) + if ("line" in props && props.line.endsWith("%")) + region.regionAnchorY = parseFloat(props.line.replace("%", "")) + if ("align" in props) + c.align = props.align as AlignSetting + c.region = region + } else { + c.line = -2; + } + return c +} + +function parse_layout_properties(s: string): undefined | Record<string, string> { + const o: Record<string, string> = {} + for (const tok of s.split(" ")) { + const [k, v, ...rest] = tok.split(":") + if (!v || rest.length) return undefined + o[k] = v + } + // some common keys to prevent false positives + if ("position" in o) return o + if ("align" in o) return o + if ("line" in o) return o + return undefined +} diff --git a/web/style/player.css b/web/style/player.css index 34e1bfb..246f7c6 100644 --- a/web/style/player.css +++ b/web/style/player.css @@ -67,69 +67,34 @@ video::cue { /* TODO this is inefficient */ /* print(", ".join([f"{x/19}em {y/19}em black" for x in range(-5,6) for y in range(-5,6) if x*x+y*y < 5*5])) */ background-color: transparent; - text-shadow: -0.21052631578947367em -0.10526315789473684em black, - -0.21052631578947367em -0.05263157894736842em black, - -0.21052631578947367em 0em black, - -0.21052631578947367em 0.05263157894736842em black, - -0.21052631578947367em 0.10526315789473684em black, - -0.15789473684210525em -0.15789473684210525em black, - -0.15789473684210525em -0.10526315789473684em black, - -0.15789473684210525em -0.05263157894736842em black, - -0.15789473684210525em 0em black, - -0.15789473684210525em 0.05263157894736842em black, - -0.15789473684210525em 0.10526315789473684em black, - -0.15789473684210525em 0.15789473684210525em black, - -0.10526315789473684em -0.21052631578947367em black, - -0.10526315789473684em -0.15789473684210525em black, - -0.10526315789473684em -0.10526315789473684em black, - -0.10526315789473684em -0.05263157894736842em black, - -0.10526315789473684em 0em black, - -0.10526315789473684em 0.05263157894736842em black, - -0.10526315789473684em 0.10526315789473684em black, - -0.10526315789473684em 0.15789473684210525em black, - -0.10526315789473684em 0.21052631578947367em black, - -0.05263157894736842em -0.21052631578947367em black, - -0.05263157894736842em -0.15789473684210525em black, - -0.05263157894736842em -0.10526315789473684em black, - -0.05263157894736842em -0.05263157894736842em black, - -0.05263157894736842em 0em black, - -0.05263157894736842em 0.05263157894736842em black, - -0.05263157894736842em 0.10526315789473684em black, - -0.05263157894736842em 0.15789473684210525em black, - -0.05263157894736842em 0.21052631578947367em black, - 0em -0.21052631578947367em black, 0em -0.15789473684210525em black, - 0em -0.10526315789473684em black, 0em -0.05263157894736842em black, - 0em 0em black, 0em 0.05263157894736842em black, - 0em 0.10526315789473684em black, 0em 0.15789473684210525em black, - 0em 0.21052631578947367em black, - 0.05263157894736842em -0.21052631578947367em black, - 0.05263157894736842em -0.15789473684210525em black, - 0.05263157894736842em -0.10526315789473684em black, - 0.05263157894736842em -0.05263157894736842em black, - 0.05263157894736842em 0em black, - 0.05263157894736842em 0.05263157894736842em black, - 0.05263157894736842em 0.10526315789473684em black, - 0.05263157894736842em 0.15789473684210525em black, - 0.05263157894736842em 0.21052631578947367em black, - 0.10526315789473684em -0.21052631578947367em black, - 0.10526315789473684em -0.15789473684210525em black, - 0.10526315789473684em -0.10526315789473684em black, - 0.10526315789473684em -0.05263157894736842em black, - 0.10526315789473684em 0em black, - 0.10526315789473684em 0.05263157894736842em black, - 0.10526315789473684em 0.10526315789473684em black, - 0.10526315789473684em 0.15789473684210525em black, - 0.10526315789473684em 0.21052631578947367em black, - 0.15789473684210525em -0.15789473684210525em black, - 0.15789473684210525em -0.10526315789473684em black, - 0.15789473684210525em -0.05263157894736842em black, - 0.15789473684210525em 0em black, - 0.15789473684210525em 0.05263157894736842em black, - 0.15789473684210525em 0.10526315789473684em black, - 0.15789473684210525em 0.15789473684210525em black, - 0.21052631578947367em -0.10526315789473684em black, - 0.21052631578947367em -0.05263157894736842em black, - 0.21052631578947367em 0em black, - 0.21052631578947367em 0.05263157894736842em black, - 0.21052631578947367em 0.10526315789473684em black; + text-shadow: 0em 0.1em black, + 0.02079116908177593em 0.09781476007338058em black, + 0.040673664307580015em 0.0913545457642601em black, + 0.058778525229247314em 0.08090169943749476em black, + 0.07431448254773941em 0.06691306063588583em black, + 0.08660254037844387em 0.05000000000000002em black, + 0.09510565162951536em 0.030901699437494747em black, + 0.09945218953682733em 0.010452846326765346em black, + 0.09945218953682734em -0.010452846326765334em black, + 0.09510565162951537em -0.030901699437494736em black, + 0.08660254037844388em -0.04999999999999998em black, + 0.07431448254773945em -0.0669130606358858em black, + 0.05877852522924733em -0.08090169943749474em black, + 0.04067366430758001em -0.09135454576426011em black, + 0.02079116908177593em -0.09781476007338058em black, + 1.2246467991473533e-17em -0.1em black, + -0.020791169081775907em -0.09781476007338058em black, + -0.04067366430757999em -0.09135454576426011em black, + -0.05877852522924731em -0.08090169943749476em black, + -0.07431448254773941em -0.06691306063588585em black, + -0.08660254037844384em -0.050000000000000044em black, + -0.09510565162951536em -0.030901699437494757em black, + -0.09945218953682733em -0.010452846326765424em black, + -0.09945218953682733em 0.010452846326765387em black, + -0.09510565162951537em 0.030901699437494726em black, + -0.08660254037844387em 0.05000000000000002em black, + -0.07431448254773941em 0.06691306063588585em black, + -0.05877852522924734em 0.08090169943749474em black, + -0.040673664307580015em 0.09135454576426011em black, + -0.020791169081775987em 0.09781476007338057em black; } |