From bb244c486807a3de6c930b030a109b67edec4c9f Mon Sep 17 00:00:00 2001 From: Thomas Friedel Date: Wed, 23 Jan 2019 17:23:47 +0100 Subject: [PATCH] separated out a Path2D type --- canvas.go | 4 +- go.mod | 2 +- go.sum | 4 +- path2d.go | 254 ++++++++++++++++++++++++++++++++++++++++++++ paths.go | 247 +++++------------------------------------- testdata/Alpha.png | Bin 1212 -> 1224 bytes testdata/Convex.png | Bin 408 -> 392 bytes testdata/Curves.png | Bin 641 -> 651 bytes testdata/Text.png | Bin 1918 -> 1915 bytes 9 files changed, 284 insertions(+), 227 deletions(-) create mode 100644 path2d.go diff --git a/canvas.go b/canvas.go index d230252..835f58a 100644 --- a/canvas.go +++ b/canvas.go @@ -19,7 +19,7 @@ type Canvas struct { x, y, w, h int fx, fy, fw, fh float64 - path path + path Path2D convex bool rect bool @@ -52,7 +52,7 @@ type drawState struct { lineDashOffset float64 scissor scissor - clip path + clip Path2D shadowColor glColor shadowOffsetX float64 diff --git a/go.mod b/go.mod index 61d5dff..53f1e41 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,5 @@ require ( golang.org/x/exp v0.0.0-20181106170214-d68db9428509 golang.org/x/image v0.0.0-20181109002202-aa35264064ba golang.org/x/mobile v0.0.0-20181026062114-a27dd33d354d - golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 // indirect + golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect ) diff --git a/go.sum b/go.sum index 62aaa66..dd19333 100644 --- a/go.sum +++ b/go.sum @@ -16,5 +16,5 @@ golang.org/x/image v0.0.0-20181109002202-aa35264064ba h1:tKfAeDKyjJZwxAJ8TPBZaf6 golang.org/x/image v0.0.0-20181109002202-aa35264064ba/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/mobile v0.0.0-20181026062114-a27dd33d354d h1:DuZZDdMFwDrzmycNhCaWSve7Vh+BIrjm7ttgb4fD3Os= golang.org/x/mobile v0.0.0-20181026062114-a27dd33d354d/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 h1:YoY1wS6JYVRpIfFngRf2HHo9R9dAne3xbkGOQ5rJXjU= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/path2d.go b/path2d.go new file mode 100644 index 0000000..ce16412 --- /dev/null +++ b/path2d.go @@ -0,0 +1,254 @@ +package canvas + +import "math" + +type Path2D struct { + p []pathPoint + move vec + cwSum float64 +} + +type pathPoint struct { + pos vec + tf vec + next vec + flags pathPointFlag +} + +type pathPointFlag uint8 + +const ( + pathMove pathPointFlag = 1 << iota + pathAttach + pathIsRect + pathIsConvex + pathIsClockwise + pathSelfIntersects +) + +// NewPath2D creates a new Path2D and returns it +func NewPath2D() *Path2D { + return &Path2D{p: make([]pathPoint, 0, 20)} +} + +// func (p *Path2D) AddPath(p2 *Path2D) { +// } + +// MoveTo (see equivalent function on canvas type) +func (p *Path2D) MoveTo(x, y float64) { + if len(p.p) > 0 && isSamePoint(p.p[len(p.p)-1].pos, vec{x, y}, 0.1) { + return + } + p.p = append(p.p, pathPoint{pos: vec{x, y}, tf: vec{x, y}, flags: pathMove}) // todo more flags probably + p.cwSum = 0 + p.move = vec{x, y} +} + +// LineTo (see equivalent function on canvas type) +func (p *Path2D) LineTo(x, y float64) { + count := len(p.p) + if count > 0 && isSamePoint(p.p[len(p.p)-1].pos, vec{x, y}, 0.1) { + return + } + if count == 0 { + p.MoveTo(x, y) + return + } + prev := &p.p[count-1] + prev.next = vec{x, y} + prev.flags |= pathAttach + p.p = append(p.p, pathPoint{pos: vec{x, y}, tf: vec{x, y}}) + newp := &p.p[count] + + px, py := prev.pos[0], prev.pos[1] + p.cwSum += (x - px) * (y + py) + cwTotal := p.cwSum + cwTotal += (p.move[0] - x) * (p.move[1] + y) + if cwTotal <= 0 { + newp.flags |= pathIsClockwise + } + + if prev.flags&pathSelfIntersects > 0 { + newp.flags |= pathSelfIntersects + } + + if len(p.p) < 4 { + newp.flags |= pathIsConvex + } else if prev.flags&pathIsConvex > 0 { + cuts := false + b0, b1 := prev.pos, vec{x, y} + for i := 1; i < count; i++ { + a0, a1 := p.p[i-1].pos, p.p[i].pos + _, r1, r2 := lineIntersection(a0, a1, b0, b1) + if r1 > 0 && r1 < 1 && r2 > 0 && r2 < 1 { + cuts = true + break + } + } + if cuts { + newp.flags |= pathSelfIntersects + } else { + prev2 := &p.p[len(p.p)-3] + cw := (newp.flags & pathIsClockwise) > 0 + + ln := prev.pos.sub(prev2.pos) + lo := vec{ln[1], -ln[0]} + dot := newp.pos.sub(prev2.pos).dot(lo) + + if (cw && dot <= 0) || (!cw && dot >= 0) { + newp.flags |= pathIsConvex + } + } + } +} + +// Arc (see equivalent function on canvas type) +func (p *Path2D) Arc(x, y, radius, startAngle, endAngle float64, anticlockwise bool) { + lastWasMove := len(p.p) == 0 || p.p[len(p.p)-1].flags&pathMove != 0 + + startAngle = math.Mod(startAngle, math.Pi*2) + if startAngle < 0 { + startAngle += math.Pi * 2 + } + endAngle = math.Mod(endAngle, math.Pi*2) + if endAngle < 0 { + endAngle += math.Pi * 2 + } + if !anticlockwise && endAngle <= startAngle { + endAngle += math.Pi * 2 + } else if anticlockwise && endAngle >= startAngle { + endAngle -= math.Pi * 2 + } + const step = math.Pi * 2 / 360 + if anticlockwise { + for a := startAngle; a > endAngle; a -= step { + s, c := math.Sincos(a) + p.LineTo(x+radius*c, y+radius*s) + } + } else { + for a := startAngle; a < endAngle; a += step { + s, c := math.Sincos(a) + p.LineTo(x+radius*c, y+radius*s) + } + } + s, c := math.Sincos(endAngle) + p.LineTo(x+radius*c, y+radius*s) + + if lastWasMove { + p.p[len(p.p)-1].flags |= pathIsConvex + } +} + +// ArcTo (see equivalent function on canvas type) +func (p *Path2D) ArcTo(x1, y1, x2, y2, radius float64) { + if len(p.p) == 0 { + return + } + p0, p1, p2 := p.p[len(p.p)-1].pos, vec{x1, y1}, vec{x2, y2} + v0, v1 := p0.sub(p1).norm(), p2.sub(p1).norm() + angle := math.Acos(v0.dot(v1)) + // should be in the range [0-pi]. if parallel, use a straight line + if angle <= 0 || angle >= math.Pi { + p.LineTo(x2, y2) + return + } + // cv are the vectors orthogonal to the lines that point to the center of the circle + cv0 := vec{-v0[1], v0[0]} + cv1 := vec{v1[1], -v1[0]} + x := cv1.sub(cv0).div(v0.sub(v1))[0] * radius + if x < 0 { + cv0 = cv0.mulf(-1) + cv1 = cv1.mulf(-1) + } + center := p1.add(v0.mulf(math.Abs(x))).add(cv0.mulf(radius)) + a0, a1 := cv0.mulf(-1).atan2(), cv1.mulf(-1).atan2() + p.Arc(center[0], center[1], radius, a0, a1, x > 0) +} + +// QuadraticCurveTo (see equivalent function on canvas type) +func (p *Path2D) QuadraticCurveTo(x1, y1, x2, y2 float64) { + if len(p.p) == 0 { + return + } + p0 := p.p[len(p.p)-1].pos + p1 := vec{x1, y1} + p2 := vec{x2, y2} + v0 := p1.sub(p0) + v1 := p2.sub(p1) + + const step = 0.01 + + for r := 0.0; r < 1; r += step { + i0 := v0.mulf(r).add(p0) + i1 := v1.mulf(r).add(p1) + pt := i1.sub(i0).mulf(r).add(i0) + p.LineTo(pt[0], pt[1]) + } + p.LineTo(x2, y2) +} + +// BezierCurveTo (see equivalent function on canvas type) +func (p *Path2D) BezierCurveTo(x1, y1, x2, y2, x3, y3 float64) { + if len(p.p) == 0 { + return + } + p0 := p.p[len(p.p)-1].pos + p1 := vec{x1, y1} + p2 := vec{x2, y2} + p3 := vec{x3, y3} + v0 := p1.sub(p0) + v1 := p2.sub(p1) + v2 := p3.sub(p2) + + const step = 0.01 + + for r := 0.0; r < 1; r += step { + i0 := v0.mulf(r).add(p0) + i1 := v1.mulf(r).add(p1) + i2 := v2.mulf(r).add(p2) + iv0 := i1.sub(i0) + iv1 := i2.sub(i1) + j0 := iv0.mulf(r).add(i0) + j1 := iv1.mulf(r).add(i1) + pt := j1.sub(j0).mulf(r).add(j0) + p.LineTo(pt[0], pt[1]) + } + p.LineTo(x3, y3) +} + +// ClosePath (see equivalent function on canvas type) +func (p *Path2D) ClosePath() { + if len(p.p) < 2 { + return + } + if isSamePoint(p.p[len(p.p)-1].pos, p.p[0].pos, 0.1) { + return + } + closeIdx := 0 + for i := len(p.p) - 1; i >= 0; i-- { + if p.p[i].flags&pathMove != 0 { + closeIdx = i + break + } + } + p.LineTo(p.p[closeIdx].pos[0], p.p[closeIdx].pos[1]) + p.p[len(p.p)-1].next = p.p[closeIdx].next + p.p[len(p.p)-1].flags |= pathAttach +} + +// Rect (see equivalent function on canvas type) +func (p *Path2D) Rect(x, y, w, h float64) { + lastWasMove := len(p.p) == 0 || p.p[len(p.p)-1].flags&pathMove != 0 + p.MoveTo(x, y) + p.LineTo(x+w, y) + p.LineTo(x+w, y+h) + p.LineTo(x, y+h) + p.LineTo(x, y) + if lastWasMove { + p.p[len(p.p)-1].flags |= pathIsRect + p.p[len(p.p)-1].flags |= pathIsConvex + } +} + +// func (p *Path2D) Ellipse(...) { +// } diff --git a/paths.go b/paths.go index 6b4d079..7c0f405 100644 --- a/paths.go +++ b/paths.go @@ -5,30 +5,6 @@ import ( "unsafe" ) -type path struct { - p []pathPoint - move vec - cwSum float64 -} - -type pathPoint struct { - pos vec - tf vec - next vec - flags pathPointFlag -} - -type pathPointFlag uint8 - -const ( - pathMove pathPointFlag = 1 << iota - pathAttach - pathIsRect - pathIsConvex - pathIsClockwise - pathSelfIntersects -) - // BeginPath clears the current path and starts a new one func (cv *Canvas) BeginPath() { if cv.path.p == nil { @@ -44,116 +20,21 @@ func isSamePoint(a, b vec, maxDist float64) bool { // MoveTo adds a gap and moves the end of the path to x/y func (cv *Canvas) MoveTo(x, y float64) { tf := cv.tf(vec{x, y}) - if len(cv.path.p) > 0 && isSamePoint(cv.path.p[len(cv.path.p)-1].tf, tf, 0.1) { - return - } - cv.path.p = append(cv.path.p, pathPoint{pos: vec{x, y}, tf: tf, flags: pathMove}) - cv.path.cwSum = 0 - cv.path.move = vec{x, y} + cv.path.MoveTo(tf[0], tf[1]) } // LineTo adds a line to the end of the path func (cv *Canvas) LineTo(x, y float64) { - count := len(cv.path.p) - if count > 0 && isSamePoint(cv.path.p[len(cv.path.p)-1].tf, cv.tf(vec{x, y}), 0.1) { - return - } - if count == 0 { - cv.path.p = append(cv.path.p, pathPoint{pos: vec{x, y}, tf: cv.tf(vec{x, y}), flags: pathMove}) - return - } - prev := &cv.path.p[count-1] tf := cv.tf(vec{x, y}) - prev.next = tf - prev.flags |= pathAttach - cv.path.p = append(cv.path.p, pathPoint{pos: vec{x, y}, tf: tf}) - newp := &cv.path.p[count] - - px, py := prev.pos[0], prev.pos[1] - cv.path.cwSum += (x - px) * (y + py) - cwTotal := cv.path.cwSum - cwTotal += (cv.path.move[0] - x) * (cv.path.move[1] + y) - if cwTotal <= 0 { - newp.flags |= pathIsClockwise - } - - if prev.flags&pathSelfIntersects > 0 { - newp.flags |= pathSelfIntersects - } - - if len(cv.path.p) < 4 { - newp.flags |= pathIsConvex - } else if prev.flags&pathIsConvex > 0 { - cuts := false - b0, b1 := prev.pos, vec{x, y} - for i := 1; i < count; i++ { - a0, a1 := cv.path.p[i-1].pos, cv.path.p[i].pos - _, r1, r2 := lineIntersection(a0, a1, b0, b1) - if r1 > 0 && r1 < 1 && r2 > 0 && r2 < 1 { - cuts = true - break - } - } - if cuts { - newp.flags |= pathSelfIntersects - } else { - prev2 := &cv.path.p[len(cv.path.p)-3] - cw := (newp.flags & pathIsClockwise) > 0 - - ln := prev.pos.sub(prev2.pos) - lo := vec{ln[1], -ln[0]} - dot := newp.pos.sub(prev2.pos).dot(lo) - - if (cw && dot <= 0) || (!cw && dot >= 0) { - newp.flags |= pathIsConvex - } - } - } + cv.path.LineTo(tf[0], tf[1]) } // Arc adds a circle segment to the end of the path. x/y is the center, radius // is the radius, startAngle and endAngle are angles in radians, anticlockwise // means that the line is added anticlockwise func (cv *Canvas) Arc(x, y, radius, startAngle, endAngle float64, anticlockwise bool) { - lastWasMove := len(cv.path.p) == 0 || cv.path.p[len(cv.path.p)-1].flags&pathMove != 0 - - startAngle = math.Mod(startAngle, math.Pi*2) - if startAngle < 0 { - startAngle += math.Pi * 2 - } - endAngle = math.Mod(endAngle, math.Pi*2) - if endAngle < 0 { - endAngle += math.Pi * 2 - } - if !anticlockwise && endAngle <= startAngle { - endAngle += math.Pi * 2 - } else if anticlockwise && endAngle >= startAngle { - endAngle -= math.Pi * 2 - } - tr := cv.tf(vec{radius, radius}) - step := 6 / math.Max(tr[0], tr[1]) - if step > 0.8 { - step = 0.8 - } else if step < 0.05 { - step = 0.05 - } - if anticlockwise { - for a := startAngle; a > endAngle; a -= step { - s, c := math.Sincos(a) - cv.LineTo(x+radius*c, y+radius*s) - } - } else { - for a := startAngle; a < endAngle; a += step { - s, c := math.Sincos(a) - cv.LineTo(x+radius*c, y+radius*s) - } - } - s, c := math.Sincos(endAngle) - cv.LineTo(x+radius*c, y+radius*s) - - if lastWasMove { - cv.path.p[len(cv.path.p)-1].flags |= pathIsConvex - } + tf := cv.tf(vec{x, y}) + cv.path.Arc(tf[0], tf[1], radius, startAngle, endAngle, anticlockwise) } // ArcTo adds to the current path by drawing a line toward x1/y1 and a circle @@ -161,123 +42,45 @@ func (cv *Canvas) Arc(x, y, radius, startAngle, endAngle float64, anticlockwise // lines from the end of the path to x1/y1, and from x1/y1 to x2/y2. The line // will only go to where the circle segment would touch the latter line func (cv *Canvas) ArcTo(x1, y1, x2, y2, radius float64) { - if len(cv.path.p) == 0 { - return - } - p0, p1, p2 := cv.path.p[len(cv.path.p)-1].pos, vec{x1, y1}, vec{x2, y2} - v0, v1 := p0.sub(p1).norm(), p2.sub(p1).norm() - angle := math.Acos(v0.dot(v1)) - // should be in the range [0-pi]. if parallel, use a straight line - if angle <= 0 || angle >= math.Pi { - cv.LineTo(x2, y2) - return - } - // cv are the vectors orthogonal to the lines that point to the center of the circle - cv0 := vec{-v0[1], v0[0]} - cv1 := vec{v1[1], -v1[0]} - x := cv1.sub(cv0).div(v0.sub(v1))[0] * radius - if x < 0 { - cv0 = cv0.mulf(-1) - cv1 = cv1.mulf(-1) - } - center := p1.add(v0.mulf(math.Abs(x))).add(cv0.mulf(radius)) - a0, a1 := cv0.mulf(-1).atan2(), cv1.mulf(-1).atan2() - cv.Arc(center[0], center[1], radius, a0, a1, x > 0) + tf1 := cv.tf(vec{x1, y1}) + tf2 := cv.tf(vec{x2, y2}) + cv.path.ArcTo(tf1[0], tf1[1], tf2[0], tf2[1], radius) } // QuadraticCurveTo adds a quadratic curve to the path. It uses the current end // point of the path, x1/y1 defines the curve, and x2/y2 is the end point func (cv *Canvas) QuadraticCurveTo(x1, y1, x2, y2 float64) { - if len(cv.path.p) == 0 { - return - } - p0 := cv.path.p[len(cv.path.p)-1].pos - p1 := vec{x1, y1} - p2 := vec{x2, y2} - v0 := p1.sub(p0) - v1 := p2.sub(p1) - - tp0, tp1, tp2 := cv.tf(p0), cv.tf(p1), cv.tf(p2) - tv0 := tp1.sub(tp0) - tv1 := tp2.sub(tp1) - - step := 1 / math.Max(math.Max(tv0[0], tv0[1]), math.Max(tv1[0], tv1[1])) - if step > 0.1 { - step = 0.1 - } else if step < 0.005 { - step = 0.005 - } - - for r := 0.0; r < 1; r += step { - i0 := v0.mulf(r).add(p0) - i1 := v1.mulf(r).add(p1) - p := i1.sub(i0).mulf(r).add(i0) - cv.LineTo(p[0], p[1]) - } + tf1 := cv.tf(vec{x1, y1}) + tf2 := cv.tf(vec{x2, y2}) + cv.path.QuadraticCurveTo(tf1[0], tf1[1], tf2[0], tf2[1]) } // BezierCurveTo adds a bezier curve to the path. It uses the current end point // of the path, x1/y1 and x2/y2 define the curve, and x3/y3 is the end point func (cv *Canvas) BezierCurveTo(x1, y1, x2, y2, x3, y3 float64) { - if len(cv.path.p) == 0 { - return - } - p0 := cv.path.p[len(cv.path.p)-1].pos - p1 := vec{x1, y1} - p2 := vec{x2, y2} - p3 := vec{x3, y3} - v0 := p1.sub(p0) - v1 := p2.sub(p1) - v2 := p3.sub(p2) - - tp0, tp1, tp2, tp3 := cv.tf(p0), cv.tf(p1), cv.tf(p2), cv.tf(p3) - tv0 := tp1.sub(tp0) - tv1 := tp2.sub(tp1) - tv2 := tp3.sub(tp2) - - step := 1 / math.Max(math.Max(math.Max(tv0[0], tv0[1]), math.Max(tv1[0], tv1[1])), math.Max(tv2[0], tv2[1])) - if step > 0.1 { - step = 0.1 - } else if step < 0.005 { - step = 0.005 - } - - for r := 0.0; r < 1; r += step { - i0 := v0.mulf(r).add(p0) - i1 := v1.mulf(r).add(p1) - i2 := v2.mulf(r).add(p2) - iv0 := i1.sub(i0) - iv1 := i2.sub(i1) - j0 := iv0.mulf(r).add(i0) - j1 := iv1.mulf(r).add(i1) - p := j1.sub(j0).mulf(r).add(j0) - cv.LineTo(p[0], p[1]) - } + tf1 := cv.tf(vec{x1, y1}) + tf2 := cv.tf(vec{x2, y2}) + tf3 := cv.tf(vec{x3, y3}) + cv.path.BezierCurveTo(tf1[0], tf1[1], tf2[0], tf2[1], tf3[0], tf3[1]) } // ClosePath closes the path to the beginning of the path or the last point // from a MoveTo call func (cv *Canvas) ClosePath() { - if len(cv.path.p) < 2 { - return - } - if isSamePoint(cv.path.p[len(cv.path.p)-1].tf, cv.path.p[0].tf, 0.1) { - return - } - closeIdx := 0 - for i := len(cv.path.p) - 1; i >= 0; i-- { - if cv.path.p[i].flags&pathMove != 0 { - closeIdx = i - break - } - } - cv.LineTo(cv.path.p[closeIdx].pos[0], cv.path.p[closeIdx].pos[1]) - cv.path.p[len(cv.path.p)-1].next = cv.path.p[closeIdx].next - cv.path.p[len(cv.path.p)-1].flags |= pathAttach + cv.path.ClosePath() } // Stroke uses the current StrokeStyle to draw the path -func (cv *Canvas) Stroke() { +func (cv *Canvas) Stroke(params ...interface{}) { + if len(params) > 0 { + if p, ok := params[0].(*Path2D); ok { + for i := range p.p { + p.p[i].tf = cv.tf(p.p[i].pos) + } + cv.stroke(p.p) + return + } + } cv.stroke(cv.path.p) } diff --git a/testdata/Alpha.png b/testdata/Alpha.png index 225c463dbf8cc983b320ace55c9c1840be3577c3..96bdd8b1755f0cc0428137cbb2be6857ae8b346c 100755 GIT binary patch delta 1204 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 delta 616 zcmeBXZDgIGQt#pE;uumf=gpjpdCdkquGcgF|37`#S#^o1Tk+f1N6Ow+&S}tLS7(|1 z?6)_MawNkfRt3VugDGEA{>rOXSe&b!v@V#fSY1DP)lQ?^1~xvne)aEd40tydSpGWK zC*?NNz5d$MopaYNwB2c5YIo*NfLqhG$o^@UXXYCv$UE<#P_sgcq zYd-Kh-#RhtFHlh^}Wl#e9M}f*6gX#2l+RpH81U&n7B1ZdsWm# z)8B``n{%Cv+P^)LGqRicmBRitUl~g$)HM00Xzg4Rv&@U=d#Wzp$Py#B_N>y diff --git a/testdata/Text.png b/testdata/Text.png index e5ba6b36545dc922a3a2a062d6f8d5d3f9374a04..8dac94d16a4be6812bd1ca6c60200aa79d8f54c1 100755 GIT binary patch delta 319 zcmV-F0l@zL4*L#}BmqLPB@+gJjt&u?P+p2GI^(=}nT=!(M$5rMNQ)P}Q_)b6rQ^2+ z=E$@0_6!VYF`dd`7?Vh8J4~5fzRGQYXPOOtn!M28*7@qE<&ZgbDB`H1`LdB`71~Oe zx0IBI`Kny!av8h;_Q+}j&gDEy07sSpjw}HjSpqn+1jv2}oe#{WRL%8&tf|6sCCgwv zkV=HX0wcORP_AU<;Z~VZu4LuW`OGh%jLFIat+t=l_Tb18z>y_@BTE2BmH_t1@^U-o zDH}M?62KB!ZNRN8krigDqLG=bB3G(hx5}{G2_HnGt(+UG$Z7fpN=HGs*2T1jwORr;0^$C;9*!p+PHg@-E41GF`GGLQ7xKEpY z+tB~!fy!wN(@tbD=cRV8F$fmzJ(D83GwA8A(6*<3KNO^LpWX>@oOZ4($idgoC-q<7 zVJo8=iAk%SC10O0&hu(Zywm65C138^F){Sf*)0;LWqTJF@z{m0l#~Df