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),
	}
}