diff --git a/geometry.go b/geometry.go
index 13a574d42f7df8b8fe6f9a0a5c1e1383f5cf7640..02a8b5199016a373956c74d9f5622512c855881b 100644
--- a/geometry.go
+++ b/geometry.go
@@ -49,6 +49,11 @@ func (u Vec) XY() (x, y float64) {
 	return u.X, u.Y
 }
 
+// Normal returns a vector normal to u (rotated by math.pi/2)
+func (u Vec) Normal() Vec {
+	return Vec{X: u.Y, Y: -u.X}
+}
+
 // Add returns the sum of vectors u and v.
 func (u Vec) Add(v Vec) Vec {
 	return Vec{
@@ -65,6 +70,14 @@ func (u Vec) Sub(v Vec) Vec {
 	}
 }
 
+// To returns the vector from vector u to vector v, equivalent to v.Sub(u).
+func (u Vec) To(v Vec) Vec {
+	return Vec{
+		v.X - u.X,
+		v.Y - u.Y,
+	}
+}
+
 // Scaled returns the vector u multiplied by c.
 func (u Vec) Scaled(c float64) Vec {
 	return Vec{u.X * c, u.Y * c}
diff --git a/imdraw/imdraw.go b/imdraw/imdraw.go
index f7ce7692de1f0d4969b07e8ad9d7013e4b0e3cbc..e616c39c4f57fd2bf0dd0f539ab2b0bd0c5bca2d 100644
--- a/imdraw/imdraw.go
+++ b/imdraw/imdraw.go
@@ -128,7 +128,9 @@ func (imd *IMDraw) Draw(t pixel.Target) {
 // Push adds some points to the IM queue. All Pushed points will have the same properties except for
 // the position.
 func (imd *IMDraw) Push(pts ...pixel.Vec) {
-	imd.Color = pixel.ToRGBA(imd.Color)
+	if _, ok := imd.Color.(pixel.RGBA); !ok {
+		imd.Color = pixel.ToRGBA(imd.Color)
+	}
 	opts := point{
 		col:       imd.Color.(pixel.RGBA),
 		pic:       imd.Picture,
@@ -495,12 +497,12 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
 				thick := pixel.V(thickness/2, 0).Rotated(normalLow)
 				imd.pushPt(lowCenter.Add(thick), pt)
 				imd.pushPt(lowCenter.Sub(thick), pt)
-				imd.pushPt(lowCenter.Sub(thick.Rotated(math.Pi/2*orientation)), pt)
+				imd.pushPt(lowCenter.Sub(thick.Normal().Scaled(orientation)), pt)
 				imd.fillPolygon()
 				thick = pixel.V(thickness/2, 0).Rotated(normalHigh)
 				imd.pushPt(highCenter.Add(thick), pt)
 				imd.pushPt(highCenter.Sub(thick), pt)
-				imd.pushPt(highCenter.Add(thick.Rotated(math.Pi/2*orientation)), pt)
+				imd.pushPt(highCenter.Add(thick.Normal().Scaled(orientation)), pt)
 				imd.fillPolygon()
 			case RoundEndShape:
 				imd.pushPt(lowCenter, pt)
@@ -528,29 +530,27 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
 
 	// first point
 	j, i := 0, 1
-	normal := points[i].pos.Sub(points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
+	ijNormal := points[0].pos.To(points[1].pos).Normal().Unit().Scaled(thickness / 2)
 
 	if !closed {
 		switch points[j].endshape {
 		case NoEndShape:
 			// nothing
 		case SharpEndShape:
-			imd.pushPt(points[j].pos.Add(normal), points[j])
-			imd.pushPt(points[j].pos.Sub(normal), points[j])
-			imd.pushPt(points[j].pos.Add(normal.Rotated(math.Pi/2)), points[j])
+			imd.pushPt(points[j].pos.Add(ijNormal), points[j])
+			imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
+			imd.pushPt(points[j].pos.Add(ijNormal.Normal()), points[j])
 			imd.fillPolygon()
 		case RoundEndShape:
 			imd.pushPt(points[j].pos, points[j])
-			imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normal.Angle(), normal.Angle()+math.Pi)
+			imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()+math.Pi)
 		}
 	}
 
-	imd.pushPt(points[j].pos.Add(normal), points[j])
-	imd.pushPt(points[j].pos.Sub(normal), points[j])
+	imd.pushPt(points[j].pos.Add(ijNormal), points[j])
+	imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
 
 	// middle points
-	// compute "previous" normal:
-	ijNormal := points[1].pos.Sub(points[0].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
 	for i := 0; i < len(points); i++ {
 		j, k := i+1, i+2
 
@@ -566,7 +566,7 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
 			k %= len(points)
 		}
 
-		jkNormal := points[k].pos.Sub(points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
+		jkNormal := points[j].pos.To(points[k].pos).Normal().Unit().Scaled(thickness / 2)
 
 		orientation := 1.0
 		if ijNormal.Cross(jkNormal) > 0 {
@@ -602,10 +602,10 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
 
 	// last point
 	i, j = len(points)-2, len(points)-1
-	normal = points[j].pos.Sub(points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
+	ijNormal = points[i].pos.To(points[j].pos).Normal().Unit().Scaled(thickness / 2)
 
-	imd.pushPt(points[j].pos.Sub(normal), points[j])
-	imd.pushPt(points[j].pos.Add(normal), points[j])
+	imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
+	imd.pushPt(points[j].pos.Add(ijNormal), points[j])
 	imd.fillPolygon()
 
 	if !closed {
@@ -613,13 +613,13 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
 		case NoEndShape:
 			// nothing
 		case SharpEndShape:
-			imd.pushPt(points[j].pos.Add(normal), points[j])
-			imd.pushPt(points[j].pos.Sub(normal), points[j])
-			imd.pushPt(points[j].pos.Add(normal.Rotated(-math.Pi/2)), points[j])
+			imd.pushPt(points[j].pos.Add(ijNormal), points[j])
+			imd.pushPt(points[j].pos.Sub(ijNormal), points[j])
+			imd.pushPt(points[j].pos.Add(ijNormal.Normal().Scaled(-1)), points[j])
 			imd.fillPolygon()
 		case RoundEndShape:
 			imd.pushPt(points[j].pos, points[j])
-			imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normal.Angle(), normal.Angle()-math.Pi)
+			imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()-math.Pi)
 		}
 	}
 
diff --git a/pixelgl/canvas.go b/pixelgl/canvas.go
index b8e40578c43d49fd9d01a008581459f405b566fc..9e1efeb342fad91f040eef22e1a8fc4e31dbb483 100644
--- a/pixelgl/canvas.go
+++ b/pixelgl/canvas.go
@@ -380,7 +380,7 @@ var canvasUniformFormat = glhf.AttrFormat{
 }
 
 var canvasVertexShader = `
-#version 330 core
+#version 130
 
 in vec2 position;
 in vec4 color;
@@ -405,7 +405,7 @@ void main() {
 `
 
 var canvasFragmentShader = `
-#version 330 core
+#version 130
 
 in vec4 Color;
 in vec2 TexCoords;
diff --git a/pixelgl/gltriangles.go b/pixelgl/gltriangles.go
index bf7a8956c34b9656d795c4e2bd2556c16b3f8920..52ed0c99dcc7837d236f52f82b0dd90297cc4875 100644
--- a/pixelgl/gltriangles.go
+++ b/pixelgl/gltriangles.go
@@ -60,9 +60,10 @@ func (gt *GLTriangles) Len() int {
 // SetLen efficiently resizes GLTriangles to len.
 //
 // Time complexity is amortized O(1).
-func (gt *GLTriangles) SetLen(len int) {
-	if len > gt.Len() {
-		needAppend := len - gt.Len()
+func (gt *GLTriangles) SetLen(length int) {
+	switch {
+	case length > gt.Len():
+		needAppend := length - gt.Len()
 		for i := 0; i < needAppend; i++ {
 			gt.data = append(gt.data,
 				0, 0,
@@ -71,11 +72,16 @@ func (gt *GLTriangles) SetLen(len int) {
 				0,
 			)
 		}
+	case length < gt.Len():
+		gt.data = gt.data[:length*gt.vs.Stride()]
+	default:
+		return
 	}
-	if len < gt.Len() {
-		gt.data = gt.data[:len*gt.vs.Stride()]
-	}
-	gt.submitData()
+	mainthread.CallNonBlock(func() {
+		gt.vs.Begin()
+		gt.vs.SetLen(length)
+		gt.vs.End()
+	})
 }
 
 // Slice returns a sub-Triangles of this GLTriangles in range [i, j).
@@ -95,16 +101,17 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) {
 	}
 
 	// TrianglesData short path
+	stride := gt.vs.Stride()
+	length := gt.Len()
 	if t, ok := t.(*pixel.TrianglesData); ok {
-		for i := 0; i < gt.Len(); i++ {
+		for i := 0; i < length; i++ {
 			var (
 				px, py = (*t)[i].Position.XY()
 				col    = (*t)[i].Color
 				tx, ty = (*t)[i].Picture.XY()
 				in     = (*t)[i].Intensity
 			)
-			s := gt.vs.Stride()
-			d := gt.data[i*s : i*s+9]
+			d := gt.data[i*stride : i*stride+9]
 			d[0] = float32(px)
 			d[1] = float32(py)
 			d[2] = float32(col.R)
@@ -119,65 +126,58 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) {
 	}
 
 	if t, ok := t.(pixel.TrianglesPosition); ok {
-		for i := 0; i < gt.Len(); i++ {
+		for i := 0; i < length; i++ {
 			px, py := t.Position(i).XY()
-			gt.data[i*gt.vs.Stride()+0] = float32(px)
-			gt.data[i*gt.vs.Stride()+1] = float32(py)
+			gt.data[i*stride+0] = float32(px)
+			gt.data[i*stride+1] = float32(py)
 		}
 	}
 	if t, ok := t.(pixel.TrianglesColor); ok {
-		for i := 0; i < gt.Len(); i++ {
+		for i := 0; i < length; i++ {
 			col := t.Color(i)
-			gt.data[i*gt.vs.Stride()+2] = float32(col.R)
-			gt.data[i*gt.vs.Stride()+3] = float32(col.G)
-			gt.data[i*gt.vs.Stride()+4] = float32(col.B)
-			gt.data[i*gt.vs.Stride()+5] = float32(col.A)
+			gt.data[i*stride+2] = float32(col.R)
+			gt.data[i*stride+3] = float32(col.G)
+			gt.data[i*stride+4] = float32(col.B)
+			gt.data[i*stride+5] = float32(col.A)
 		}
 	}
 	if t, ok := t.(pixel.TrianglesPicture); ok {
-		for i := 0; i < gt.Len(); i++ {
+		for i := 0; i < length; i++ {
 			pic, intensity := t.Picture(i)
-			gt.data[i*gt.vs.Stride()+6] = float32(pic.X)
-			gt.data[i*gt.vs.Stride()+7] = float32(pic.Y)
-			gt.data[i*gt.vs.Stride()+8] = float32(intensity)
+			gt.data[i*stride+6] = float32(pic.X)
+			gt.data[i*stride+7] = float32(pic.Y)
+			gt.data[i*stride+8] = float32(intensity)
 		}
 	}
 }
 
-func (gt *GLTriangles) submitData() {
+// Update copies vertex properties from the supplied Triangles into this GLTriangles.
+//
+// The two Triangles (gt and t) must be of the same len.
+func (gt *GLTriangles) Update(t pixel.Triangles) {
+	if gt.Len() != t.Len() {
+		panic(fmt.Errorf("(%T).Update: invalid triangles len", gt))
+	}
+	gt.updateData(t)
+
 	// this code is supposed to copy the vertex data and CallNonBlock the update if
 	// the data is small enough, otherwise it'll block and not copy the data
 	if len(gt.data) < 256 { // arbitrary heurestic constant
 		data := append([]float32{}, gt.data...)
 		mainthread.CallNonBlock(func() {
 			gt.vs.Begin()
-			dataLen := len(data) / gt.vs.Stride()
-			gt.vs.SetLen(dataLen)
 			gt.vs.SetVertexData(data)
 			gt.vs.End()
 		})
 	} else {
 		mainthread.Call(func() {
 			gt.vs.Begin()
-			dataLen := len(gt.data) / gt.vs.Stride()
-			gt.vs.SetLen(dataLen)
 			gt.vs.SetVertexData(gt.data)
 			gt.vs.End()
 		})
 	}
 }
 
-// Update copies vertex properties from the supplied Triangles into this GLTriangles.
-//
-// The two Triangles (gt and t) must be of the same len.
-func (gt *GLTriangles) Update(t pixel.Triangles) {
-	if gt.Len() != t.Len() {
-		panic(fmt.Errorf("(%T).Update: invalid triangles len", gt))
-	}
-	gt.updateData(t)
-	gt.submitData()
-}
-
 // Copy returns an independent copy of this GLTriangles.
 //
 // The returned Triangles are *GLTriangles as the underlying type.
diff --git a/pixelgl/window.go b/pixelgl/window.go
index afbd5f7c7ac76cfb1d9c66791705910381bd3200..76523a5ce287b8325af970a20d68dba370749f27 100644
--- a/pixelgl/window.go
+++ b/pixelgl/window.go
@@ -90,10 +90,8 @@ func NewWindow(cfg WindowConfig) (*Window, error) {
 	err := mainthread.CallErr(func() error {
 		var err error
 
-		glfw.WindowHint(glfw.ContextVersionMajor, 3)
-		glfw.WindowHint(glfw.ContextVersionMinor, 3)
-		glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
-		glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
+		glfw.WindowHint(glfw.ContextVersionMajor, 2)
+		glfw.WindowHint(glfw.ContextVersionMinor, 1)
 
 		glfw.WindowHint(glfw.Resizable, bool2int[cfg.Resizable])
 		glfw.WindowHint(glfw.Decorated, bool2int[!cfg.Undecorated])