diff --git a/README.md b/README.md index 31fba1c..f79e919 100644 --- a/README.md +++ b/README.md @@ -113,13 +113,15 @@ These features *should* work just like their HTML5 counterparts, but there are l - getImageData - putImageData - clearRect +- shadowColor +- shadowOffset(X/Y) # Missing features - globalCompositeOperation - lineDashOffset - miterLimit -- shadows +- shadowBlur - textBaseline - getLineDash - isPointInPath diff --git a/canvas.go b/canvas.go index 355bef1..5ff65ff 100644 --- a/canvas.go +++ b/canvas.go @@ -45,6 +45,11 @@ type drawState struct { scissor scissor clip []pathPoint + + shadowColor glColor + shadowOffsetX float64 + shadowOffsetY float64 + /* The current transformation matrix. The current clipping region. @@ -99,6 +104,7 @@ func New(x, y, w, h int) *Canvas { cv := &Canvas{stateStack: make([]drawState, 0, 20)} cv.SetBounds(x, y, w, h) cv.state.lineWidth = 1 + cv.state.lineAlpha = 1 cv.state.globalAlpha = 1 cv.state.fill.color = glColor{a: 1} cv.state.stroke.color = glColor{a: 1} @@ -169,19 +175,20 @@ loop: const alphaTexSize = 2048 var ( - gli GL - buf uint32 - alphaTex uint32 - sr *solidShader - lgr *linearGradientShader - rgr *radialGradientShader - ipr *imagePatternShader - sar *solidAlphaShader - rgar *radialGradientAlphaShader - lgar *linearGradientAlphaShader - ipar *imagePatternAlphaShader - ir *imageShader - glChan = make(chan func()) + gli GL + buf uint32 + shadowBuf uint32 + alphaTex uint32 + sr *solidShader + lgr *linearGradientShader + rgr *radialGradientShader + ipr *imagePatternShader + sar *solidAlphaShader + rgar *radialGradientAlphaShader + lgar *linearGradientAlphaShader + ipar *imagePatternAlphaShader + ir *imageShader + glChan = make(chan func()) ) // LoadGL needs to be called once per GL context to load the GL assets @@ -280,6 +287,12 @@ func LoadGL(glimpl GL) (err error) { return } + gli.GenBuffers(1, &shadowBuf) + err = glError() + if err != nil { + return + } + gli.ActiveTexture(gl_TEXTURE0) gli.GenTextures(1, &alphaTex) gli.BindTexture(gl_TEXTURE_2D, alphaTex) @@ -610,3 +623,27 @@ func (cv *Canvas) Transform(a, b, c, d, e, f float64) { func (cv *Canvas) SetTransform(a, b, c, d, e, f float64) { cv.state.transform = mat{a, b, 0, c, d, 0, e, f, 1} } + +// SetShadowColor sets the color of the shadow. If it is fully transparent (default) +// then no shadow is drawn +func (cv *Canvas) SetShadowColor(color ...interface{}) { + if c, ok := parseColor(color...); ok { + cv.state.shadowColor = c + } +} + +// SetShadowOffsetX sets the x offset of the shadow +func (cv *Canvas) SetShadowOffsetX(offset float64) { + cv.state.shadowOffsetX = offset +} + +// SetShadowOffsetY sets the y offset of the shadow +func (cv *Canvas) SetShadowOffsetY(offset float64) { + cv.state.shadowOffsetY = offset +} + +// SetShadowOffset sets the offset of the shadow +func (cv *Canvas) SetShadowOffset(x, y float64) { + cv.state.shadowOffsetX = x + cv.state.shadowOffsetY = y +} diff --git a/images.go b/images.go index 9330c59..bc57504 100644 --- a/images.go +++ b/images.go @@ -250,6 +250,24 @@ func (cv *Canvas) DrawImage(image interface{}, coords ...float64) { p2 := cv.tf(vec{dx + dw, dy + dh}) p3 := cv.tf(vec{dx + dw, dy}) + if cv.state.shadowColor.a != 0 { + tris := [24]float32{ + 0, 0, + float32(cv.fw), 0, + float32(cv.fw), float32(cv.fh), + 0, 0, + float32(cv.fw), float32(cv.fh), + 0, float32(cv.fh), + float32(p0[0]), float32(p0[1]), + float32(p3[0]), float32(p3[1]), + float32(p2[0]), float32(p2[1]), + float32(p0[0]), float32(p0[1]), + float32(p2[0]), float32(p2[1]), + float32(p1[0]), float32(p1[1]), + } + cv.drawShadow(tris[:]) + } + gli.StencilFunc(gl_EQUAL, 0, 0xFF) gli.BindBuffer(gl_ARRAY_BUFFER, buf) diff --git a/paths.go b/paths.go index d79fc75..a8e524b 100644 --- a/paths.go +++ b/paths.go @@ -360,6 +360,10 @@ func (cv *Canvas) stroke(path []pathPoint) { gli.BindBuffer(gl_ARRAY_BUFFER, buf) gli.BufferData(gl_ARRAY_BUFFER, len(tris)*4, unsafe.Pointer(&tris[0]), gl_STREAM_DRAW) + cv.drawShadow(tris) + + gli.BindBuffer(gl_ARRAY_BUFFER, buf) + gli.ColorMask(false, false, false, false) gli.StencilFunc(gl_ALWAYS, 1, 0xFF) gli.StencilOp(gl_REPLACE, gl_REPLACE, gl_REPLACE) @@ -513,6 +517,10 @@ func (cv *Canvas) Fill() { gli.BindBuffer(gl_ARRAY_BUFFER, buf) gli.BufferData(gl_ARRAY_BUFFER, len(tris)*4, unsafe.Pointer(&tris[0]), gl_STREAM_DRAW) + cv.drawShadow(tris) + + gli.BindBuffer(gl_ARRAY_BUFFER, buf) + gli.ColorMask(false, false, false, false) gli.StencilFunc(gl_ALWAYS, 1, 0xFF) gli.StencilOp(gl_REPLACE, gl_REPLACE, gl_REPLACE) @@ -700,6 +708,24 @@ func (cv *Canvas) FillRect(x, y, w, h float64) { p2 := cv.tf(vec{x + w, y + h}) p3 := cv.tf(vec{x + w, y}) + if cv.state.shadowColor.a != 0 { + tris := [24]float32{ + 0, 0, + float32(cv.fw), 0, + float32(cv.fw), float32(cv.fh), + 0, 0, + float32(cv.fw), float32(cv.fh), + 0, float32(cv.fh), + float32(p0[0]), float32(p0[1]), + float32(p3[0]), float32(p3[1]), + float32(p2[0]), float32(p2[1]), + float32(p0[0]), float32(p0[1]), + float32(p2[0]), float32(p2[1]), + float32(p1[0]), float32(p1[1]), + } + cv.drawShadow(tris[:]) + } + gli.BindBuffer(gl_ARRAY_BUFFER, buf) data := [8]float32{float32(p0[0]), float32(p0[1]), float32(p1[0]), float32(p1[1]), float32(p2[0]), float32(p2[1]), float32(p3[0]), float32(p3[1])} gli.BufferData(gl_ARRAY_BUFFER, len(data)*4, unsafe.Pointer(&data[0]), gl_STREAM_DRAW) diff --git a/shadows.go b/shadows.go new file mode 100644 index 0000000..46eddd2 --- /dev/null +++ b/shadows.go @@ -0,0 +1,95 @@ +package canvas + +import ( + "image" + "unsafe" +) + +func (cv *Canvas) drawShadow(tris []float32) { + if len(tris) == 0 || cv.state.shadowColor.a == 0 { + return + } + + ox, oy := float32(cv.state.shadowOffsetX), float32(cv.state.shadowOffsetY) + + count := len(tris) + for i := 12; i < count; i += 2 { + tris[i] += ox + tris[i+1] += oy + } + + gli.BindBuffer(gl_ARRAY_BUFFER, shadowBuf) + gli.BufferData(gl_ARRAY_BUFFER, len(tris)*4, unsafe.Pointer(&tris[0]), gl_STREAM_DRAW) + + gli.ColorMask(false, false, false, false) + gli.StencilFunc(gl_ALWAYS, 1, 0xFF) + gli.StencilOp(gl_REPLACE, gl_REPLACE, gl_REPLACE) + gli.StencilMask(0x01) + + gli.UseProgram(sr.id) + gli.Uniform4f(sr.color, 0, 0, 0, 0) + gli.Uniform2f(sr.canvasSize, float32(cv.fw), float32(cv.fh)) + + gli.EnableVertexAttribArray(sr.vertex) + gli.VertexAttribPointer(sr.vertex, 2, gl_FLOAT, false, 0, 0) + gli.DrawArrays(gl_TRIANGLES, 6, int32(len(tris)/2-6)) + gli.DisableVertexAttribArray(sr.vertex) + + gli.ColorMask(true, true, true, true) + + gli.StencilFunc(gl_EQUAL, 1, 0xFF) + + var style drawStyle + style.color = cv.state.shadowColor + + vertex := cv.useShader(&style) + gli.EnableVertexAttribArray(vertex) + gli.VertexAttribPointer(vertex, 2, gl_FLOAT, false, 0, 0) + gli.DrawArrays(gl_TRIANGLES, 0, 6) + gli.DisableVertexAttribArray(vertex) + + gli.StencilOp(gl_KEEP, gl_KEEP, gl_KEEP) + gli.StencilFunc(gl_ALWAYS, 0, 0xFF) + + gli.Clear(gl_STENCIL_BUFFER_BIT) + gli.StencilMask(0xFF) +} + +func (cv *Canvas) drawTextShadow(offset image.Point, strWidth, strHeight int, x, y float64) { + x += cv.state.shadowOffsetX + y += cv.state.shadowOffsetY + + gli.StencilFunc(gl_EQUAL, 0, 0xFF) + + gli.BindBuffer(gl_ARRAY_BUFFER, buf) + + var style drawStyle + style.color = cv.state.shadowColor + + vertex, alphaTexCoord := cv.useAlphaShader(&style, 1) + + gli.EnableVertexAttribArray(vertex) + gli.EnableVertexAttribArray(alphaTexCoord) + + p0 := cv.tf(vec{float64(offset.X) + x, float64(offset.Y) + y}) + p1 := cv.tf(vec{float64(offset.X) + x, float64(offset.Y+strHeight) + y}) + p2 := cv.tf(vec{float64(offset.X+strWidth) + x, float64(offset.Y+strHeight) + y}) + p3 := cv.tf(vec{float64(offset.X+strWidth) + x, float64(offset.Y) + y}) + + tw := float64(strWidth) / alphaTexSize + th := float64(strHeight) / alphaTexSize + data := [16]float32{float32(p0[0]), float32(p0[1]), float32(p1[0]), float32(p1[1]), float32(p2[0]), float32(p2[1]), float32(p3[0]), float32(p3[1]), + 0, 1, 0, float32(1 - th), float32(tw), float32(1 - th), float32(tw), 1} + gli.BufferData(gl_ARRAY_BUFFER, len(data)*4, unsafe.Pointer(&data[0]), gl_STREAM_DRAW) + + gli.VertexAttribPointer(vertex, 2, gl_FLOAT, false, 0, 0) + gli.VertexAttribPointer(alphaTexCoord, 2, gl_FLOAT, false, 0, 8*4) + gli.DrawArrays(gl_TRIANGLE_FAN, 0, 4) + + gli.DisableVertexAttribArray(vertex) + gli.DisableVertexAttribArray(alphaTexCoord) + + gli.ActiveTexture(gl_TEXTURE0) + + gli.StencilFunc(gl_ALWAYS, 0, 0xFF) +} diff --git a/text.go b/text.go index 747ad89..a71ba30 100644 --- a/text.go +++ b/text.go @@ -183,23 +183,24 @@ func (cv *Canvas) FillText(str string, x, y float64) { x -= float64(strWidth) } + gli.ActiveTexture(gl_TEXTURE1) + gli.BindTexture(gl_TEXTURE_2D, alphaTex) + for y := 0; y < strHeight; y++ { + off := y * textImage.Stride + gli.TexSubImage2D(gl_TEXTURE_2D, 0, 0, int32(alphaTexSize-1-y), int32(strWidth), 1, gl_ALPHA, gl_UNSIGNED_BYTE, gli.Ptr(&textImage.Pix[off])) + } + + cv.drawTextShadow(textOffset, strWidth, strHeight, x, y) + gli.StencilFunc(gl_EQUAL, 0, 0xFF) gli.BindBuffer(gl_ARRAY_BUFFER, buf) vertex, alphaTexCoord := cv.useAlphaShader(&cv.state.fill, 1) - gli.ActiveTexture(gl_TEXTURE1) - gli.BindTexture(gl_TEXTURE_2D, alphaTex) - gli.EnableVertexAttribArray(vertex) gli.EnableVertexAttribArray(alphaTexCoord) - for y := 0; y < strHeight; y++ { - off := y * textImage.Stride - gli.TexSubImage2D(gl_TEXTURE_2D, 0, 0, int32(alphaTexSize-1-y), int32(strWidth), 1, gl_ALPHA, gl_UNSIGNED_BYTE, gli.Ptr(&textImage.Pix[off])) - } - p0 := cv.tf(vec{float64(textOffset.X) + x, float64(textOffset.Y) + y}) p1 := cv.tf(vec{float64(textOffset.X) + x, float64(textOffset.Y+strHeight) + y}) p2 := cv.tf(vec{float64(textOffset.X+strWidth) + x, float64(textOffset.Y+strHeight) + y}) @@ -215,16 +216,16 @@ func (cv *Canvas) FillText(str string, x, y float64) { gli.VertexAttribPointer(alphaTexCoord, 2, gl_FLOAT, false, 0, 8*4) gli.DrawArrays(gl_TRIANGLE_FAN, 0, 4) - for y := 0; y < strHeight; y++ { - gli.TexSubImage2D(gl_TEXTURE_2D, 0, 0, int32(alphaTexSize-1-y), int32(strWidth), 1, gl_ALPHA, gl_UNSIGNED_BYTE, gli.Ptr(&zeroes[0])) - } - gli.DisableVertexAttribArray(vertex) gli.DisableVertexAttribArray(alphaTexCoord) gli.ActiveTexture(gl_TEXTURE0) gli.StencilFunc(gl_ALWAYS, 0, 0xFF) + + for y := 0; y < strHeight; y++ { + gli.TexSubImage2D(gl_TEXTURE_2D, 0, 0, int32(alphaTexSize-1-y), int32(strWidth), 1, gl_ALPHA, gl_UNSIGNED_BYTE, gli.Ptr(&zeroes[0])) + } } // TextMetrics is the result of a MeasureText call