/
TimeFliesElmish.fs
133 lines (110 loc) · 3.63 KB
/
TimeFliesElmish.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
module TimeFliesElmish
open System
open Fable
open Fable.Core
open Fable.Reaction
open FSharp.Control
open Browser
module Cmd =
type Cmd<'Msg> = (('Msg -> unit) -> unit) list
let ofMsg msg: Cmd<'Msg> = [ fun d -> d msg ]
let ofAsync (action: _ -> Async<'Msg>): Cmd<'Msg> =
[ fun dispatch ->
async {
let! msg = action dispatch
dispatch msg
}
|> Async.StartImmediate ]
type Letter = { char: char; x: int; y: int }
type Model =
{ letters: Map<int, Letter>
fps: int
second: float
count: int
message: string
stream: IDisposable option }
type Msg =
| Letter of index: int * char: char * x: int * y: int
| Message of string
| Stream of IDisposable
let getOffset (element: Browser.Types.Element) =
let doc = element.ownerDocument
let docElem = doc.documentElement
let clientTop = docElem.clientTop
let clientLeft = docElem.clientLeft
let scrollTop = window.pageYOffset
let scrollLeft = window.pageXOffset
int (scrollTop - clientTop), int (scrollLeft - clientLeft)
let startStream (text: string) (dispatch: Msg -> unit) =
let container = document.body
let top, left = getOffset container
asyncRx {
let chars =
Seq.toList text
|> Seq.mapi (fun i c -> i, c)
let! i, c = AsyncRx.ofSeq chars
yield!
AsyncRx.ofMouseMove ()
|> AsyncRx.delay (100 * i)
|> AsyncRx.requestAnimationFrame
|> AsyncRx.map (fun m -> Letter(i, c, int m.clientX + i * 10 + 15 - left, int m.clientY - top))
}
|> AsyncRx.toObservable
|> Observable.subscribe dispatch
let disposeStream (model: Model) =
model.stream |> Option.iter (fun d -> d.Dispose())
let update (msg: Msg) (model: Model) =
match msg with
| Letter (i, c, x, y) ->
let second = DateTimeOffset.Now.ToUnixTimeSeconds() |> float
{ model with
letters = Map.add i { char = c; x = x; y = y } model.letters
second = second
fps = if second > model.second then model.count else model.fps
count = if second > model.second then 0 else model.count + 1 },
[]
| Message txt ->
disposeStream model
{ model with
letters = Map.empty
message = txt },
[fun dispatch ->
startStream txt dispatch |> Stream |> dispatch]
| Stream stream -> { model with stream = Some stream }, []
let init msg =
{ letters = Map.empty
count = 0
second = 0.
fps = 0
message = msg
stream = None },
Cmd.ofMsg (Message msg)
open Feliz
[<ReactComponent>]
let TimeFliesElmish (text: string) =
let model, dispatch =
ReactStore.useElmishStore init update disposeStream text
Html.div [
prop.style [
style.fontFamily "Consolas, monospace"
style.height 100
]
prop.children [
for KeyValue(_idx, letter) in model.letters do
Html.span [
prop.style [
style.position.fixedRelativeToWindow
style.top letter.y
style.left letter.x
]
prop.text (string letter.char)
]
Html.input [
prop.style [ style.width 300 ]
prop.defaultValue model.message
prop.onChange (fun (ev: Types.Event) ->
Message (ev.target :?> Types.HTMLInputElement).value |> dispatch)
]
Html.p ("fps: " + string model.fps)
]
]