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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
|
// Package shader is what crystal-shader knows about the lock: the messages
// swaylock sends (see renderer.c in crystal-swaylock.patch) turned into the
// uniforms crystal.frag reads. No GPU, no I/O, no clock of its own: every
// call takes the time, so the tests can drive it.
//
// Times are seconds since crystal-shader started (iTime). An event that
// never happened is at Never, far enough in the past that every effect of it
// has ended.
package shader
import (
"math"
"strconv"
"strings"
)
// Never is the time of an event that has not happened.
const Never = -1000.0
// How long the picture still changes after an event, so rendering goes on
// for at least that long even at activity 0.
const (
verifyEndEffect = 0.5
capsEffect = 0.6 // fading the Caps Lock glow in or out
wrongEffect = 2.5 // the slam: shut, hold, reopen
unlockEffect = 10.0 // until swaylock ends the lock and us with it
)
// How fast iMotionTime runs, relative to iTime.
const (
keyKick = 1.0 // typing energy added per key press
keyDecay = 0.8 // seconds for typing energy to fall to 1/e
keySpeedup = 9.0 // extra speed at full typing energy
keySaturation = 4.0 // typing energy at which the speed-up is ~76%
wrongSpeed = 0.04 // speed after a wrong password ...
wrongHold = 0.5 // ... held for this long ...
wrongRecover = 0.6 // ... then easing back to normal over this
)
// maxFrameGap is the longest frame-to-frame step that still moves the
// shards: after a pause the step spans the whole pause, and the shards were
// not moving then, so they must not catch up.
const maxFrameGap = 0.25
// Output is one lock surface, as swaylock announced it.
type Output struct {
ID uint32
Width, Height int // buffer pixels
// pointer position in buffer pixels, origin top left; valid if HasPointer
PointerX, PointerY float64
HasPointer bool
}
// State is everything the uniforms are made of.
type State struct {
// activity moves linearly from activityFrom to ActivityTarget over
// activityDuration seconds, starting at activityStart
Activity float64
ActivityTarget float64
activityFrom float64
activityDuration float64
activityStart float64
KeyTime, VerifyStart, VerifyEnd, WrongTime, UnlockTime float64
// the first frame: the lock opens from the plain wallpaper
LockTime float64
CapsTime float64
CapsLock bool
capsKnown bool
WrongCount int
// MotionTime is the clock all motion runs on, integrated frame by frame
// so that changes of speed never make the motion jump.
MotionTime float64
// typing energy: every key press adds keyKick, it decays with keyDecay.
// Stored as the value at typingAt.
typing float64
typingAt float64
// Holding: the lock was taken just before the system sleeps. The
// picture stays at the start of the lock-in (the plain wallpaper)
// until Release, so that the glass breaks when the user is back.
Holding bool
// keep rendering until this time, even at activity 0
busyUntil float64
// activity reached 0 and the frame showing that has been drawn: draw
// nothing until something happens
Idle bool
Outputs map[uint32]*Output
lastFrame float64
drawn bool
}
// New is the state when the lock starts: active, nothing happened yet.
func New() *State {
return &State{
Activity: 1, ActivityTarget: 1, activityFrom: 1,
KeyTime: Never, VerifyStart: Never, VerifyEnd: Never,
WrongTime: Never, UnlockTime: Never, LockTime: Never,
CapsTime: Never, typingAt: Never, busyUntil: Never,
lastFrame: Never,
Outputs: map[uint32]*Output{},
}
}
func (s *State) keepBusy(now, seconds float64) {
s.busyUntil = math.Max(s.busyUntil, now+seconds)
s.Idle = false
}
func (s *State) updateActivity(now float64) {
if s.activityDuration <= 0 {
s.Activity = s.ActivityTarget
return
}
t := (now - s.activityStart) / s.activityDuration
switch {
case t >= 1:
s.Activity = s.ActivityTarget
case t <= 0:
s.Activity = s.activityFrom
default:
s.Activity = s.activityFrom + (s.ActivityTarget-s.activityFrom)*t
}
}
// Verifying is whether the password is being checked.
func (s *State) Verifying() bool { return s.VerifyStart > s.VerifyEnd }
// MotionSpeed is how fast iMotionTime runs at time t, relative to iTime: a
// bit faster after each key press, much faster when typing fast, and very
// slow for a moment after a wrong password.
func (s *State) MotionSpeed(t float64) float64 {
speed := 1.0
if age := t - s.typingAt; age >= 0 {
energy := s.typing * math.Exp(-age/keyDecay)
speed += keySpeedup * math.Tanh(energy/keySaturation)
}
if age := t - s.WrongTime; age >= 0 && age < wrongHold+wrongRecover {
r := 0.0
if age >= wrongHold {
r = (age - wrongHold) / wrongRecover
}
r = r * r * (3 - 2*r) // smoothstep
speed = wrongSpeed + (speed-wrongSpeed)*r
}
return speed
}
// Apply takes one message from swaylock, received at now. Unknown or
// malformed messages are ignored: the channel only ever makes the picture
// different, never the lock.
func (s *State) Apply(now float64, msg string) {
f := strings.Fields(msg)
if len(f) == 0 {
return
}
num := func(i int) (float64, bool) {
if i >= len(f) {
return 0, false
}
v, err := strconv.ParseFloat(f[i], 64)
return v, err == nil && !math.IsNaN(v) && !math.IsInf(v, 0)
}
id := func(i int) (uint32, bool) {
if i >= len(f) {
return 0, false
}
v, err := strconv.ParseUint(f[i], 10, 32)
return uint32(v), err == nil
}
switch f[0] {
case "key", "verify":
// someone is typing: whatever we were waiting for, it is over
s.Release(now)
}
switch f[0] {
case "key":
age := now - s.typingAt
if age >= 0 {
s.typing = s.typing*math.Exp(-age/keyDecay) + keyKick
} else {
s.typing = keyKick
}
s.typingAt = now
s.KeyTime = now
// keep rendering while the speed-up is noticeable
s.keepBusy(now, keyDecay*math.Log(1+s.typing*20))
case "verify":
s.VerifyStart = now
s.keepBusy(now, 0)
case "verify-done":
s.VerifyEnd = now
s.keepBusy(now, verifyEndEffect)
case "wrong":
s.WrongTime = now
s.keepBusy(now, wrongEffect)
case "unlocked":
s.UnlockTime = now
s.keepBusy(now, unlockEffect)
case "unlock-cancelled":
// the system went to sleep during the unlock: locked again
s.UnlockTime = Never
s.keepBusy(now, 0)
case "attempts":
if a, ok := num(1); ok {
n := int(math.Max(0, math.Min(1000, a)))
if n != s.WrongCount {
s.WrongCount = n
// one frame to show the new glass, even at activity 0
s.keepBusy(now, 0)
}
}
case "capslock":
if a, ok := num(1); ok {
on := a != 0
switch {
case !s.capsKnown:
// the state when the lock starts: show it at once
s.capsKnown = true
s.CapsLock = on
s.keepBusy(now, 0)
case on != s.CapsLock:
s.CapsLock = on
s.CapsTime = now
s.keepBusy(now, capsEffect)
}
}
case "activity":
target, ok1 := num(1)
ms, ok2 := num(2)
if !ok1 || !ok2 {
return
}
s.updateActivity(now)
s.activityFrom = s.Activity
s.ActivityTarget = math.Max(0, math.Min(1, target))
s.activityDuration = math.Max(0, ms/1000)
s.activityStart = now
s.updateActivity(now)
if s.ActivityTarget > 0 {
s.Idle = false
}
case "pointer":
o, ok := id(1)
x, ok2 := num(2)
y, ok3 := num(3)
if !ok || !ok2 || !ok3 {
return
}
// the pointer is on one output at a time; the others lose it
for _, out := range s.Outputs {
out.HasPointer = out.ID == o
if out.HasPointer {
out.PointerX, out.PointerY = x, y
}
}
// redraw even when paused, so the pointer follows
s.keepBusy(now, 0)
case "pointer-leave":
if o, ok := id(1); ok {
if out := s.Outputs[o]; out != nil {
out.HasPointer = false
}
s.keepBusy(now, 0)
}
case "output":
o, ok := id(1)
w, ok2 := num(2)
h, ok3 := num(3)
if !ok || !ok2 || !ok3 || w < 1 || h < 1 || w > 1<<15 || h > 1<<15 {
return
}
out := s.Outputs[o]
if out == nil {
out = &Output{ID: o}
s.Outputs[o] = out
}
out.Width, out.Height = int(w), int(h)
s.keepBusy(now, 0)
case "output-gone":
if o, ok := id(1); ok {
delete(s.Outputs, o)
}
}
}
// Frame advances the clocks to a frame drawn at now, and says whether to
// draw it at all.
func (s *State) Frame(now float64) bool {
if s.Idle {
return false
}
s.updateActivity(now)
if s.LockTime == Never || s.Holding {
// while holding, the lock-in starts anew with every frame
s.LockTime = now
}
if dt := now - s.lastFrame; dt > 0 && dt < maxFrameGap {
s.MotionTime += dt * s.MotionSpeed(now)
}
s.lastFrame = now
return true
}
// Drawn notes that the frame of now is on its way to the screen. At
// activity 0 the shader shows its resting picture; once that is drawn,
// nothing needs drawing until something happens.
func (s *State) Drawn(now float64) {
if s.Holding && now > s.busyUntil {
s.Idle = true
}
if s.Activity <= 0 && s.ActivityTarget <= 0 && !s.Verifying() && now > s.busyUntil {
s.Idle = true
}
}
// Release ends Holding: the lock-in starts with the next frame drawn, not
// now. After a resume the compositor asks for frames only once the screens
// are on again, and the lock-in should play then, not into a dark panel.
func (s *State) Release(now float64) {
if !s.Holding {
return
}
s.Holding = false
s.LockTime = Never
s.keepBusy(now, 0)
}
// Uniforms are the values crystal.frag reads, for one output. The layout
// matches struct uniforms in gl.h.
type Uniforms struct {
Resolution [2]float32
Time float32
TimeDelta float32
Frame int32
Mouse [4]float32
Activity float32
KeyTime float32
VerifyStart float32
VerifyEnd float32
WrongTime float32
UnlockTime float32
LockTime float32
MotionTime float32
WrongCount int32
CapsLock int32
CapsTime float32
Seed float32
}
// Uniforms for output o at time now.
func (s *State) Uniforms(o *Output, now, dt float64, frame int, seed float64) Uniforms {
w, h := float32(o.Width), float32(o.Height)
// Shadertoy's iMouse has its origin bottom left; z is 1 if the pointer
// is on this output (Shadertoy: button down, which never happens here)
mouse := [4]float32{w / 2, h / 2, 0, 0}
if o.HasPointer {
mouse = [4]float32{float32(o.PointerX), h - float32(o.PointerY), 1, 0}
}
caps := int32(0)
if s.CapsLock {
caps = 1
}
return Uniforms{
Resolution: [2]float32{w, h},
Time: float32(now),
TimeDelta: float32(dt),
Frame: int32(frame),
Mouse: mouse,
Activity: float32(s.Activity),
KeyTime: float32(s.KeyTime),
VerifyStart: float32(s.VerifyStart),
VerifyEnd: float32(s.VerifyEnd),
WrongTime: float32(s.WrongTime),
UnlockTime: float32(s.UnlockTime),
LockTime: float32(s.LockTime),
MotionTime: float32(s.MotionTime),
WrongCount: int32(s.WrongCount),
CapsLock: caps,
CapsTime: float32(s.CapsTime),
Seed: float32(seed),
}
}
|