mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-06 13:38:28 -04:00
screenshot: initial support for scroll capture mode
fixes #2871 port 1.5
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mirrors handleScrollFrame's stitch logic so glides run without a compositor
|
||||
type simSession struct {
|
||||
prevSig []float32
|
||||
prevPlaced bool
|
||||
unmatched bool
|
||||
unmatchedTicks int
|
||||
st *stitcher
|
||||
}
|
||||
|
||||
func (s *simSession) observe(rows []byte) {
|
||||
cols := s.st.rowSamples(rows)
|
||||
sig := s.st.frameSig(rows)
|
||||
dup := duplicateFrame(sig, s.prevSig)
|
||||
s.prevSig = sig
|
||||
|
||||
switch {
|
||||
case dup && s.unmatched:
|
||||
s.unmatchedTicks++
|
||||
if s.unmatchedTicks < scrollSeamTicks {
|
||||
return
|
||||
}
|
||||
if _, placed := s.st.pushFrame(rows, cols); !placed {
|
||||
s.st.seamAppend(rows, cols)
|
||||
}
|
||||
s.prevPlaced = true
|
||||
s.unmatched = false
|
||||
s.unmatchedTicks = 0
|
||||
case dup && s.prevPlaced:
|
||||
return
|
||||
default:
|
||||
_, placed := s.st.pushFrame(rows, cols)
|
||||
s.prevPlaced = placed
|
||||
s.unmatched = !placed
|
||||
s.unmatchedTicks = 0
|
||||
}
|
||||
}
|
||||
|
||||
// the page at a fractional scroll offset, as a compositor renders mid-glide
|
||||
func fractionalFrame(page []byte, stride, frameH int, offset float64) []byte {
|
||||
top := int(offset)
|
||||
frac := offset - float64(top)
|
||||
out := make([]byte, frameH*stride)
|
||||
for y := 0; y < frameH; y++ {
|
||||
a := page[(top+y)*stride : (top+y+1)*stride]
|
||||
b := page[(top+y+1)*stride : (top+y+2)*stride]
|
||||
row := out[y*stride : (y+1)*stride]
|
||||
for x := range row {
|
||||
row[x] = byte(float64(a[x])*(1-frac) + float64(b[x])*frac)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// blank gaps between paragraphs plus identical card blocks repeated around
|
||||
func webbyPage(rng *rand.Rand, stride, rows int) []byte {
|
||||
page := make([]byte, rows*stride)
|
||||
card := make([]byte, 40*stride)
|
||||
rng.Read(card)
|
||||
|
||||
row := 0
|
||||
for row < rows {
|
||||
switch rng.Intn(4) {
|
||||
case 0: // blank gap
|
||||
row += 10 + rng.Intn(20)
|
||||
case 1: // repeated card block
|
||||
n := copy(page[row*stride:], card)
|
||||
row += n / stride
|
||||
default: // paragraph of distinct rows
|
||||
n := (8 + rng.Intn(22)) * stride
|
||||
if row*stride+n > len(page) {
|
||||
n = len(page) - row*stride
|
||||
}
|
||||
rng.Read(page[row*stride : row*stride+n])
|
||||
row += n / stride
|
||||
}
|
||||
}
|
||||
return page
|
||||
}
|
||||
|
||||
// screen-fixed sidebar in the unsampled outer 8% plus per-frame hover noise
|
||||
func addFixedChrome(rng *rand.Rand, frame []byte, stride, frameH int, sidebar []byte) {
|
||||
sbw := len(sidebar) / frameH
|
||||
for y := 0; y < frameH; y++ {
|
||||
copy(frame[y*stride:y*stride+sbw], sidebar[y*sbw:(y+1)*sbw])
|
||||
}
|
||||
hoverTop := 40 + rng.Intn(frameH-80)
|
||||
for y := hoverTop; y < hoverTop+24; y++ {
|
||||
off := y*stride + stride/3
|
||||
for x := 0; x < 60; x++ {
|
||||
frame[off+x] ^= 0x08
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// starting at the page bottom and scrolling up must prepend, never stall
|
||||
func TestScrollSimulationBottomUp(t *testing.T) {
|
||||
const stride = 2048
|
||||
const frameH = 240
|
||||
rng := rand.New(rand.NewSource(99))
|
||||
page := webbyPage(rng, stride, 4000)
|
||||
|
||||
st := newStitcher(stride)
|
||||
sidebar := make([]byte, frameH*140)
|
||||
rng.Read(sidebar)
|
||||
|
||||
sim := &simSession{st: st}
|
||||
|
||||
pos := 3700.0
|
||||
capture := func() []byte {
|
||||
f := fractionalFrame(page, stride, frameH, pos)
|
||||
addFixedChrome(rng, f, stride, frameH, sidebar)
|
||||
return f
|
||||
}
|
||||
glide := func(target float64) {
|
||||
for i := 0; ; i++ {
|
||||
step := (target - pos) * 0.45
|
||||
if step > -1 && step < 1 {
|
||||
break
|
||||
}
|
||||
pos += step
|
||||
if i%4 != 3 {
|
||||
pos = float64(int(pos))
|
||||
}
|
||||
sim.observe(capture())
|
||||
}
|
||||
pos = target
|
||||
sim.observe(capture())
|
||||
sim.observe(capture())
|
||||
}
|
||||
|
||||
sim.observe(capture())
|
||||
for _, target := range []float64{3640, 3560, 3460, 3340, 3240} {
|
||||
glide(target)
|
||||
}
|
||||
|
||||
wantRows := (3700 + frameH) - 3240
|
||||
got := sim.st.rows()
|
||||
if got < wantRows-stitchMinAppend || got > wantRows+2 {
|
||||
t.Fatalf("canvas has %d rows, want ~%d (upward scrolling must prepend)", got, wantRows)
|
||||
}
|
||||
topPage := 3240 + (wantRows - got)
|
||||
for _, cr := range []int{0, 100, 300} {
|
||||
if !rowMatchesPage(sim.st.canvas, page, stride, cr, topPage+cr) {
|
||||
t.Fatalf("canvas row %d does not map onto page row %d", cr, topPage+cr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// exact page row or a blend of neighbors, allowing a one-row offset
|
||||
func rowMatchesPage(canvas, page []byte, stride, canvasRow, pageRow int) bool {
|
||||
for x := 200; x < stride-1400; x++ {
|
||||
c := int(canvas[canvasRow*stride+x])
|
||||
lo, hi := 255, 0
|
||||
for k := pageRow - 1; k <= pageRow+1; k++ {
|
||||
v := int(page[k*stride+x])
|
||||
lo, hi = min(lo, v), max(hi, v)
|
||||
}
|
||||
if c < lo-1 || c > hi+1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// a fling past a full frame height must seam a new segment, not go dead
|
||||
func TestScrollSimulationFastFlingRecovers(t *testing.T) {
|
||||
const stride = 2048
|
||||
const frameH = 240
|
||||
rng := rand.New(rand.NewSource(7))
|
||||
page := webbyPage(rng, stride, 4000)
|
||||
|
||||
sim := &simSession{st: newStitcher(stride)}
|
||||
frame := func(top int) []byte {
|
||||
return slices.Clone(page[top*stride : (top+frameH)*stride])
|
||||
}
|
||||
rest := func(top int) {
|
||||
for range scrollSeamTicks + 2 {
|
||||
sim.observe(frame(top))
|
||||
}
|
||||
}
|
||||
|
||||
rest(0)
|
||||
sim.observe(frame(60))
|
||||
sim.observe(frame(130))
|
||||
rest(130)
|
||||
firstRange := 130 + frameH
|
||||
|
||||
sim.observe(frame(900))
|
||||
sim.observe(frame(1400))
|
||||
rest(1800)
|
||||
|
||||
sim.observe(frame(1860))
|
||||
sim.observe(frame(1930))
|
||||
rest(1930)
|
||||
|
||||
wantRows := firstRange + (1930 - 1800) + frameH
|
||||
if got := sim.st.rows(); got != wantRows {
|
||||
t.Fatalf("canvas has %d rows, want %d (first range %d + new segment)", got, wantRows, firstRange)
|
||||
}
|
||||
seamStart := firstRange
|
||||
if !slices.Equal(sim.st.canvas[seamStart*stride:], page[1800*stride:(1930+frameH)*stride]) {
|
||||
t.Fatal("new segment content wrong after fling recovery")
|
||||
}
|
||||
}
|
||||
|
||||
// eased glides with up/down scrubbing must cover the range exactly once
|
||||
func TestScrollSimulationSmoothGlide(t *testing.T) {
|
||||
const stride = 2048
|
||||
const frameH = 240
|
||||
rng := rand.New(rand.NewSource(99))
|
||||
page := webbyPage(rng, stride, 4000)
|
||||
|
||||
st := newStitcher(stride)
|
||||
sidebar := make([]byte, frameH*140)
|
||||
rng.Read(sidebar)
|
||||
|
||||
sim := &simSession{st: st}
|
||||
|
||||
pos := 0.0
|
||||
capture := func() []byte {
|
||||
f := fractionalFrame(page, stride, frameH, pos)
|
||||
addFixedChrome(rng, f, stride, frameH, sidebar)
|
||||
return f
|
||||
}
|
||||
glide := func(target float64) {
|
||||
for i := 0; ; i++ {
|
||||
step := (target - pos) * 0.45
|
||||
if step > -1 && step < 1 {
|
||||
break
|
||||
}
|
||||
pos += step
|
||||
// mostly snapped to device pixels, with the odd fractional frame
|
||||
if i%4 != 3 {
|
||||
pos = float64(int(pos))
|
||||
}
|
||||
sim.observe(capture())
|
||||
}
|
||||
pos = target
|
||||
sim.observe(capture())
|
||||
sim.observe(capture())
|
||||
}
|
||||
|
||||
sim.observe(capture())
|
||||
for _, target := range []float64{160, 330, 480, 650, 800, 960, 1100} {
|
||||
glide(target)
|
||||
}
|
||||
for _, target := range []float64{700, 300, 900, 1100} {
|
||||
glide(target)
|
||||
}
|
||||
|
||||
wantRows := 1100 + frameH
|
||||
got := sim.st.rows()
|
||||
if got < wantRows-stitchMinAppend || got > wantRows+2 {
|
||||
t.Fatalf("canvas has %d rows, want ~%d (more = duplicated bands, fewer = gaps)", got, wantRows)
|
||||
}
|
||||
|
||||
hoverLo, hoverHi := stride/3, stride/3+60
|
||||
mismatched := 0
|
||||
for row := 0; row < min(got, wantRows); row += 7 {
|
||||
off := row * stride
|
||||
a1, b1 := sim.st.canvas[off+200:off+hoverLo], page[off+200:off+hoverLo]
|
||||
a2, b2 := sim.st.canvas[off+hoverHi:off+stride], page[off+hoverHi:off+stride]
|
||||
if !slices.Equal(a1, b1) || !slices.Equal(a2, b2) {
|
||||
mismatched++
|
||||
}
|
||||
}
|
||||
if mismatched > (wantRows/7)/20 {
|
||||
t.Fatalf("%d of %d sampled rows mismatch page content (mid-animation pixels baked in)", mismatched, wantRows/7)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user