mic_none

Module:Sandbox/Bawolff/canvas Source: en.wikipedia.org/wiki/Module:Sandbox/Bawolff/canvas

-- The {{qif}} of drawing pictures.

local p = {}
local metatable = {}
local methodtable = {}
local create2dContext
local setDefaults
local isFinite

function p.getContext( contextType, contextAttributes )
	if contextType == '2d' then
		ctx = create2dContext()

		if type( contextAttributes ) == 'table' then
			if contextAttributes.width ~= nil then
				ctx._width = tonumber(contextAttributes.width)
			end
			if contextAttributes.height ~= nil then
				ctx._height = tonumber(contextAttributes.height)
			end
			if type( contextAttributes.containerClass ) == 'string' then
				ctx._containerClass = contextAttributes.containerClass
			end
			if type( contextAttributes.containerStyle ) == 'string' then
				ctx._containerStyle = contextAttributes.containerStyle
			end
			if contextAttributes.alpha == false then
				ctx._alpha = false
			end
		end
		return ctx
	end
	error( "Unsupported context" )
end

function p.demo( frame )
	local ctx = p.getContext( '2d', { width=600, height=600 } )

	-- draw some eyes
	ctx:moveTo( 280, 100 )
	ctx:quadraticCurveTo( 290, 110, 280, 120 )
	ctx:quadraticCurveTo( 270, 110, 280, 100 )

	ctx:moveTo( 320, 100 )
	ctx:quadraticCurveTo( 330, 110, 320, 120 )
	ctx:quadraticCurveTo( 310, 110, 320, 100 )
	ctx:fill()

	-- A mouth
	ctx.fillStyle = 'pink'
	ctx:beginPath()
	ctx:moveTo( 250, 160 )
	ctx:bezierCurveTo( 260, 200, 340, 185, 350, 160 )
	ctx:bezierCurveTo( 340, 180, 260, 180, 250, 160 )
	ctx:fill()

	-- You can also use SVG paths. Taken from Example.svg
	ctx:setTransform( 6.3951354,0,0,6.3951354,-22.626246,-7.1082509 )
	local path = p.Path2D( "M 17.026327,63.789847 C 0.7506376,64.058469 13.88279,66.387154 13.113883,69.323258 C 8.0472417,70.287093 3.5936285,63.565714 6.8090451,59.370548 C 8.7591553,55.717791 15.269922,55.198361 16.902068,59.393261 C 17.532581,60.758947 17.628237,62.396589 17.026327,63.789847 z M 15.306463,62.656109 C 18.852566,58.713773 7.6543584,56.609143 10.765803,61.304742 C 12.124789,62.217715 13.961359,61.705342 15.306463,62.656109 z M 31.307931,62.391383 C 27.130518,63.524026 24.669863,68.663004 27.470717,72.229472 C 25.946657,74.052316 24.253697,71.076237 24.857281,69.636909 C 23.737444,67.038428 17.399862,72.254246 19.386636,68.888657 C 23.159719,67.551193 22.398496,63.711301 22.06067,60.848671 C 24.064085,60.375294 24.370376,65.772689 27.167918,63.326048 C 28.350126,62.546369 29.927362,61.067531 31.307931,62.391383 z M 37.66875,70.598623 C 33.467314,66.62264 32.517064,77.972723 37.30626,74.466636 C 38.742523,73.853608 40.55904,70.38932 37.66875,70.598623 z M 41.677321,70.973131 C 42.340669,75.308182 36.926157,78.361257 33.331921,76.223155 C 29.43435,74.893988 30.618698,67.677232 35.003806,68.567885 C 37.137393,70.592854 42.140265,67.002221 37.656192,66.290007 C 35.242233,65.914214 35.166503,62.640757 38.036954,63.926404 C 40.847923,64.744926 43.227838,68.124735 41.677321,70.973131 z M 62.379099,76.647079 C 62.007404,78.560417 61.161437,84.034535 58.890565,82.010019 C 59.769679,79.039958 62.536382,72.229115 56.947899,72.765789 C 53.790416,73.570863 54.908257,80.968388 51.529286,79.496859 C 51.707831,76.559817 55.858125,71.896837 50.8321,70.678504 C 45.898113,69.907818 47.485944,75.735824 45.286883,78.034703 C 42.916393,76.333396 45.470823,71.647155 46.624124,69.414735 C 50.919507,67.906486 63.618534,70.878704 62.379099,76.647079 z M 66.426447,83.84905 C 67.616398,85.777591 62.114624,94.492698 62.351124,90.31711 C 63.791684,86.581961 65.730376,78.000636 67.391891,74.85575 C 71.027815,73.781175 76.383067,75.350289 76.591972,79.751898 C 77.048545,83.793048 73.066803,88.429945 68.842187,86.765936 C 67.624386,86.282034 66.56741,85.195132 66.426447,83.84905 z M 74.086569,81.803435 C 76.851893,78.050524 69.264402,74.310256 67.560734,78.378191 C 65.893402,80.594099 67.255719,83.775746 69.700555,84.718558 C 72.028708,85.902224 73.688639,83.888662 74.086569,81.803435 z M 82.318799,73.124577 C 84.30523,75.487059 81.655015,88.448086 78.247183,87.275736 C 78.991935,82.387828 81.291029,77.949394 82.318799,73.124577 z M 95.001985,87.684695 C 78.726298,87.953319 91.858449,90.281999 91.089542,93.218107 C 86.0229,94.18194 81.569287,87.460562 84.784701,83.265394 C 86.734814,79.612637 93.245582,79.09321 94.877729,83.28811 C 95.508245,84.653796 95.603892,86.291438 95.001985,87.684695 z M 93.282122,86.550957 C 96.828223,82.608621 85.630017,80.503993 88.741461,85.199592 C 90.100447,86.112565 91.937018,85.600192 93.282122,86.550957 z " )
	ctx.fillStyle = 'red'
	ctx:beginPath()
	ctx:fill(path)
	return tostring(ctx)
end

-- Round to 0. To prevent 1.13132e-14 from showing up.
local function r0(x)
	if math.abs(x) < 1e-4 then
		return 0
	end
	return x
end

local function normalizeAngle( angle )
	return ((angle % (math.pi*2)) + math.pi*2) % (math.pi*2)
end

metatable.__index = methodtable

metatable.__tostring = function( t )
	return t:getWikitext()	
end

local pathmethods = {}
local pathmeta = {}
pathmeta.__index = pathmethods
setmetatable( methodtable, pathmeta )


function create2dContext()
	local ctx = {}
	setmetatable( ctx, metatable )

	ctx._width = 300 
	ctx._height = 300
	ctx._containerClass = nil
	ctx._containerStyle = nil
	ctx._alpha = true
	
	-- Default values
	setDefaults( ctx )
	return ctx
end

setDefaults = function( ctx )
	ctx.__stateStack = {}
	ctx.__operations = {}

	ctx._currentTransform = { 1, 0, 0, 1, 0, 0 }
	ctx._path = ""
	ctx._fillRule = "nonzero"
	ctx._lineDash = {}
	ctx.lineWidth = 1.0
	ctx.lineCap = 'butt'
	ctx.lineJoin = 'miter'
	ctx.miterLimit = 10
	ctx.lineDashOffset = 0.0
	ctx.font = "10px sans-serif"
	ctx.textAlign = 'start'
	ctx.textBaseline = 'alphabetic'
	ctx.direction = 'inherit'
	ctx.letterSpacing = '0px'
	ctx.fontKerning = 'auto'
	ctx.fontStretch = 'normal'
	ctx.fontVariantCaps = 'normal'
	ctx.textRendering = 'auto'
	ctx.wordSpacing = '0px'
	ctx.fillStyle = '#000'
	ctx.strokeStyle = '#000'
	ctx.shadowBlur = 0
	ctx.shadowColor = 'rgb(0 0 0 / 0%)'
	ctx.shadowOffsetX = 0
	ctx.shadowOffsetY = 0
	ctx.globalAlpha = 1.0
	ctx.globalCompositeOperation = "source-over"
	ctx.imageSmoothingEnabled = true
	ctx.imageSmoothingQuality = "low"
	ctx.canvas = nil
	ctx.filter = "none"
	return ctx
end

local newOperation = function( t, operation )
	op = {}
	op.name = operation
	op._path = t._path
	op._currentTransform = mw.clone(t._currentTransform)
	op._fillRule = t._fillRule
	op._lineDash = t._lineDash

	op.lineWidth = t.lineWidth 
	op.lineCap = t.lineCap 
	op.lineJoin = t.lineJoin 
	op.miterLimit = t.miterLimit 
	op.lineDashOffset = t.lineDashOffset 
	op.font = t.font 
	op.textAlign = t.textAlign 
	op.textBaseline = t.textBaseline 
	op.direction = t.direction 
	op.letterSpacing = t.letterSpacing 
	op.fontKerning = t.fontKerning 
	op.fontStretch = t.fontStretch 
	op.fontVariantCaps = t.fontVariantCaps 
	op.textRendering = t.textRendering 
	op.wordSpacing = t.wordSpacing 
	op.fillStyle = t.fillStyle 
	op.strokeStyle = t.strokeStyle 
	op.shadowBlur = t.shadowBlur 
	op.shadowColor = t.shadowColor 
	op.shadowOffsetX = t.shadowOffsetX 
	op.shadowOffsetY = t.shadowOffsetY 
	op.globalAlpha = t.globalAlpha 
	op.globalCompositeOperation = t.globalCompositeOperation 
	op.imageSmoothingEnabled = t.imageSmoothingEnabled 
	op.imageSmoothingQuality = t.imageSmoothingQuality 
	op.canvas = t.canvas 
	op.filter = t.filter
	return op
end

methodtable.setTransform = function( ctx, a, b, c, d, e, f )
	-- last 0 0 1 row is left implied
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	ctx:resetTransform()
	ctx:transform( a, b, c, d, e, f )
end


methodtable.resetTransform = function( ctx )
	-- last 0 0 1 row is left implied
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	ctx._currentTransform = { 1, 0, 0, 1, 0, 0 }
end

methodtable.transform = function( ctx, a, b, c, d, e, f )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	-- Do a matrix multiply
	-- a c e
	-- b d f
	-- 0 0 1

	local oa = ctx._currentTransform[1]
	local ob = ctx._currentTransform[2]
	local oc = ctx._currentTransform[3]
	local od = ctx._currentTransform[4]
	local oe = ctx._currentTransform[5]
	local of = ctx._currentTransform[6]
	
	ctx._currentTransform[1] = a*oa + b*oc
	ctx._currentTransform[3] = c*oa + d*oc
	ctx._currentTransform[5] = e*oa + f*oc + oe
	ctx._currentTransform[2] = a*ob + b*od
	ctx._currentTransform[4] = c*ob + d*od
	ctx._currentTransform[6] = e*ob + f*od + of
	
end

methodtable.scale = function( ctx, x, y )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	assert( type(x) == "number", "x argument to scale must be a number" )
	assert( type(y) == "number", "y argument to scale must be a number" )
	ctx:transform( x, 0, 0, y, 0, 0 )
end

methodtable.rotate = function( ctx, a )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	assert( type(a) == "number", "Argument a to rotate must be number of radians" )
	ctx:transform( math.cos(a), math.sin(a), -math.sin(a), math.cos(a), 0, 0 )
end

methodtable.translate = function( ctx, x, y )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	ctx:transform( 1, 0, 0, 1, x, y )
end

methodtable.setLineDash = function( ctx, dashArray )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	assert( type(dashArray) == 'table', 'dashArray (second arg) should be an array' )

	local newDash = {}
	for i, v in ipairs( dashArray ) do
		if type(v) ~= 'number' or v <= 0 or v == 1/0 or v~=v then
			-- Normally I would throw an error here, but the canvas spec
			-- says you aren't allowed to
			mw.log( "Invalid lineDash set. Ignoring" )
			return
		end
		newDash[#newDash+1] = v
	end
	-- Must always be even.
	if #newDash % 2 == 1 then
		for i, v in ipairs( dashArray ) do
			newDash[#newDash+1] = v
		end
	end
	ctx._lineDash = newDash
end

methodtable.getLineDash = function( ctx, dashArray )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	return ctx._lineDash
end


-- Path2D methods

p.Path2D = function( pathDesc )
	local path = {}
	path._path = ""
	setmetatable( path, pathmeta )
	if type( pathDesc ) == "string" then
		-- Constructor can take an SVG path description
		path._path = pathDesc
	end
	if type( pathDesc ) == 'table' and type( pathDesc._path ) == 'string' then
		-- Constructor can take a Path2D object.
		path._path = pathDesc._path
	end
	return path
end

-- Technically this is only supposed to be on Path2D and not context.
pathmethods.addPath = function( ctx, path, transform )
	assert( type(ctx) == 'table' and type(ctx._path) == 'string', "First argument must be a CanvasRenderingContext2D or Path2D object. Did you use '.' instead of ':'?" )
	if transform ~= nil then
		error( "transform argument to addPath is not implemented yet" )
	end

	if path == nil or path._path == nil then
		error( "Second argument should be a Path2D object" )
	end
	ctx._path = ctx._path .. " " .. path._path
end

pathmethods.moveTo = function( ctx, x, y )
	assert( type(ctx) == 'table' and type(ctx._path) == 'string', "First argument must be a CanvasRenderingContext2D or Path2D object. Did you use '.' instead of ':'?" )
	ctx._path = ctx._path .. string.format( "M %.8g %.8g", r0(x), r0(y) )
end

pathmethods.lineTo = function( ctx, x, y )
	assert( type(ctx) == 'table' and type(ctx._path) == 'string', "First argument must be a CanvasRenderingContext2D or Path2D object. Did you use '.' instead of ':'?" )
	ctx._path = ctx._path .. string.format( "L %.8g %.8g", r0(x), r0(y) )
end

pathmethods.quadraticCurveTo = function( ctx, cx, cy, x, y )
	assert( type(ctx) == 'table' and type(ctx._path) == 'string', "First argument must be a CanvasRenderingContext2D or Path2D object. Did you use '.' instead of ':'?" )
	ctx._path = ctx._path .. " Q " .. cx .. " " .. cy .. " " .. x .. " " .. y
end

pathmethods.bezierCurveTo = function( ctx, c1x, c1y, c2x, c2y, x, y )
	assert( type(ctx) == 'table' and type(ctx._path) == 'string', "First argument must be a CanvasRenderingContext2D or Path2D object. Did you use '.' instead of ':'?" )
	ctx._path = ctx._path .. " C " .. c1x .. " " .. c1y .. " " .. c2x .. " " .. c2y .. " " .. x .. " " .. y
end


pathmethods.beginPath = function( ctx )
	assert( type(ctx) == 'table' and type(ctx._path) == 'string', "First argument must be a CanvasRenderingContext2D or Path2D object. Did you use '.' instead of ':'?" )
	ctx._path = ''
end

pathmethods.closePath = function( ctx )
	assert( type(ctx) == 'table' and type(ctx._path) == 'string', "First argument must be a CanvasRenderingContext2D or Path2D object. Did you use '.' instead of ':'?" )
	ctx._path = ctx._path .. ' Z'
end

pathmethods.rect = function( ctx, x, y, w, h )
	assert( type(ctx) == 'table' and type(ctx._path) == 'string', "First argument must be a CanvasRenderingContext2D or Path2D object. Did you use '.' instead of ':'?" )
	if not isFinite( x ) or not isFinite( y ) or not isFinite( w ) or not isFinite( h ) or w == 0 or h == 0 then
		return
	end
	ctx:moveTo( x, y )
	ctx:lineTo( x+w, y )
	ctx:lineTo( x+w, y+h )
	ctx:lineTo( x, y+h )
	ctx:closePath()
end

-- FIXME, behaviour around if a path is closed without calling closePath() is not correct.
-- Draw an arc centered on (x,y). counterClockWise argument is optional and defaults false.
pathmethods.arc = function( ctx, x, y, radius, startAngle, endAngle, counterClockWise )
	assert( type(ctx) == 'table' and type(ctx._path) == 'string', "First argument must be a CanvasRenderingContext2D or Path2D object. Did you use '.' instead of ':'?" )

	if radius < 0 then
		error( "IndexSizeError: radius cannot be negative" )
	end

	-- FIXME test the case of a full circle.
	-- Seems like full circle if endAngle-startAngle >= 2pi when CW and startAngle-endAngle >= 2pi when CCW
	-- but not normalized. e.g. if startAngle is 10pi and endAngle is pi, that is full circle in CCW but not CW
	if counterCLockwise == true then
		if endAngle-startAngle >= math.pi*2 then
			startAngle=math.pi*2
			endAngle=0
		end
	else
		if endAngle-startAngle >= math.pi*2 then
			startAngle=math.pi*2
			endAngle=0
		end
	end

	startAngle = normalizeAngle( startAngle )
	endAngle = normalizeAngle( endAngle )
	

	local startX = x + math.cos( startAngle )*radius
	local startY = y + math.sin( startAngle )*radius
	local endX = x + math.cos( endAngle )*radius
	local endY = y + math.sin( endAngle )*radius
	local circle = false
	assert( startX == startX and endX == endX and startY == startY and endY == endY, "NaN detected when calculating angle" )
	if startX == endX and startY == endY then
		-- SVG arc command doesn't like drawing perfect circles
		endX = endX + 0.01
		endY = endY + 0.01
	end
	local large, ccw

	-- FIXME, if there is not subpath yet, the lineTo() should be a moveTo().
	if counterClockWise == true then
		ccw = 1
		if normalizeAngle( startAngle - endAngle ) > math.pi then
			large = 1
			if startAngle < endAngle or ( startAngle > 3*math.pi/4 and endAngle < math.pi/4 ) then
				startX, endX = endX, startX
				startY, endY = endY, startY
			end
		else
			if startAngle > endAngle and not( startAngle > 3*math.pi/4 and endAngle < math.pi/4 ) then
				startX, endX = endX, startX
				startY, endY = endY, startY
			end
			large = 0
		end
	else
		ccw = 0
		if normalizeAngle( startAngle - endAngle ) > math.pi then
			large = 0
			if startAngle < endAngle or ( startAngle > 3*math.pi/4 and endAngle < math.pi/4 ) then
				startX, endX = endX, startX
				startY, endY = endY, startY
			end
		else
			if startAngle > endAngle and not( startAngle > 3*math.pi/4 and endAngle < math.pi/4 ) then
				startX, endX = endX, startX
				startY, endY = endY, startY
			end
			large = 1
		end
	end

	-- FIXME, is this equivalent to the need-new-subpath flag in spec?
	if ctx._path == '' then
		ctx:moveTo( startX, startY )
	end
	ctx:lineTo( startX, startY )
	ctx:addPath( p.Path2D( string.format(
		"A %.8g %.8g 0 %d %d %.8g %.8g",
		r0(radius),
		r0(radius),
		large,
		ccw,
		r0(endX),
		r0(endY)
	)))

	if circle then
		ctx:lineTo( startX, startY )
	end
end

pathmethods.arcTo = function( ctx, x1, y1, x2, y2, radius )
	assert( type(ctx) == 'table' and type(ctx._path) == 'string', "First argument must be a CanvasRenderingContext2D or Path2D object. Did you use '.' instead of ':'?" )

	x0, y0 = string.match( ctx._path, "(%-?[0-9.]+)%s+(%-?[0-9.]+)%s*$")
	if x0 == nil or y0 == nil then
		-- FIXME, this isn't right if the last item was z (closePath()).
		if ctx._path ~= '' then
			mw.log( "FIXME: arcTo() might be broken in this case" )
		end
		ctx:moveTo( x1, y1 )
		x0 = x1
		y0 = y1
	end
	x0 = tonumber(x0)
	y0 = tonumber(y0)

	assert( radius >= 0, "IndexSizeError: radius must be positive in arcTo()" )

	if
		( x0 == x1 and y0 == y1 ) or
		( x1 == x2 and y1 == y2 ) or
		radius == 0 or
		( x0 == x1 and x1 == x2 ) or
		( y0 == y1 and y1 == y2 )
	then
		ctx:lineTo( x1, y1 )
		return
	end

	local angle1 = math.atan2( y1-y0, x1-x0 )
	local angle2 = math.atan2( y2-y1, x2-x1 )
	local avgAngle = (math.abs(angle1)+math.abs(angle2))/2
	local amtOfLine1 = radius/math.tan(avgAngle)

	local curveStartX = x1 - math.cos(angle1)*amtOfLine1
	local curveStartY = y1 - math.sin(angle1)*amtOfLine1
	local curveEndX = x1 + math.cos(angle2)*amtOfLine1
	local curveEndY = y1 + math.sin(angle2)*amtOfLine1

	local ccw = 0

	if angle2 > angle1 then
		ccw = 1
	end

	ctx:lineTo( curveStartX, curveStartY )
	ctx:addPath( p.Path2D( string.format(
		"A %.8g %.8g 0 %d %d %.8g %.8g",
		r0(radius),
		r0(radius),
		0, -- large
		ccw,
		r0(curveEndX),
		r0(curveEndY)
	)))

end

pathmethods.ellipse = function( ctx, x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterClockWise )
	assert( type(ctx) == 'table' and type(ctx._path) == 'string', "First argument must be a CanvasRenderingContext2D or Path2D object. Did you use '.' instead of ':'?" )
	if radiusX == radiusY and rotation == 0 then
		-- Easy case.
		ctx:arc( x, y, radiusX, startAngle, endAngle, counterClockWise )
		return
	end
	error( "FIXME. ellipse is not implemented yet." )
end

pathmethods.roundRect = function( ctx, x, y, w, h, radii )
	assert( type(ctx) == 'table' and type(ctx._path) == 'string', "First argument must be a CanvasRenderingContext2D or Path2D object. Did you use '.' instead of ':'?" )

	if not( isFinite( x ) ) or not( isFinite( y ) ) or not( isFinite( w ) ) or not( isFinite( h ) ) then
		-- per spec, silently ignore.
		return
	end
	
	if type( radii ) == 'number' then
		radii = { radii }
	end
	assert( type( radii ) == 'table' and #radii >= 1 and #radii <= 4, 'RangeError: invalid radii' )
	for i, v in ipairs( radii ) do
		if type( v ) == 'table' and type( v[1] ) == 'number' and v[1] == v[2] then
			radii[i] = v[1]
		elseif type( v ) == 'table' and type( v.x ) == 'number' and v.x == v.y then
			radii[i] =v.x
		elseif type( v ) ~= 'number' then
			-- FIXME todo.
			error( "Using ellipse corners for roundRect is not currently supported" )
		end
	end
	local topLeftR, topRightR, bottomLeftR, bottomRightR
	if #radii == 1 then
		topLeftR, topRightR, bottomLeftR, bottomRightR = radii[1], radii[1], radii[1], radii[1]
	elseif #radii == 2 then
		topLeftR, topRightR, bottomLeftR, bottomRightR = radii[1], radii[2], radii[2], radii[1]
	elseif #radii == 3 then
		topLeftR, topRightR, bottomLeftR, bottomRightR = radii[1], radii[2], radii[2], radii[3]
	elseif #radii == 4 then
		topLeftR, topRightR, bottomLeftR, bottomRightR = radii[1], radii[2], radii[4], radii[3]
	else
		error( "invalid radius" )
	end

	local top, bottom, left, right = topRightR + topLeftR, bottomRightR + bottomLeftR, topLeftR + bottomLeftR, topRightR + bottomRightR
	local scale = math.min( w/top, h/left, h/right, w/bottom )
	if scale < 1 then
		topLeftR = topLeftR * scale
		topRightR = topRightR * scale
		bottomLeftR = bottomLeftR * scale
		bottomRightR = bottomRightR * scale
	end
	local ccw = 1
	if (w >= 0 and h < 0) or (w < 0 and h >= 0) then
		ccw = 0
	end

	local function addArc( radius, endX, endY )
		ctx:addPath( p.Path2D( string.format(
			"A %.8g %.8g 0 %d %d %.8g %.8g",
			r0(radius),
			r0(radius),
			0, -- large flag
			ccw,
			r0(endX),
			r0(endY)
		)))
	end

	ctx:beginPath()
	ctx:moveTo( x + topLeftR, y )
	ctx:lineTo( x + w - topRightR, y )
	addArc( topRightR, x+w, y + topRightR )
	ctx:lineTo( x + w, y + h - bottomRightR )
	addArc( bottomRightR, x+w-bottomRightR, y+h )
	ctx:lineTo( x+bottomLeftR, y+h)
	addArc(bottomLeftR, x, y+h-bottomLeftR )
	ctx:lineTo(x, y+topLeftR )
	addArc( topLeftR, x+topLeftR, y )
	ctx:closePath()
	ctx:moveTo( x, y )

end


-- End of Path2D methods

-- can be fill(fillRule), fill(path), fill(path, fillRule)
methodtable.fill = function( ctx, arg1, arg2 )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	local op = newOperation( ctx, 'fill' )
	if arg2 == 'evenodd' or arg1 == 'evenodd' then
		op._fillRule = 'evenodd'
	end
	if type( arg1 ) == 'table' and type( arg1._path ) == 'string' then
		op._path = arg1._path
	end
	table.insert( ctx.__operations, op )
end


-- returns an iterator
local function parsePath(path)
	-- FIXME, in the path syntax, you can technically omit spaces, which doesn't work here.
	-- https://www.w3.org/TR/SVG11/paths.html#PathData
	local getNextEntry = string.gmatch( ctx._path, "(%a)%s*([-+0-9eE., ]+)" )
	local curType = ''
	local points = {}
	return function()
		while true do
			local curTypeU = curType:upper()
			if (curTypeU == 'L' or curTypeU == 'M' or curTypeU == 'T' ) and #points >= 2 then
				return curType, { tonumber(table.remove( points, 1 )), tonumber(table.remove( points, 1 )) }
			elseif ( curTypeU == 'Z' ) then
				points = {}
				curType = ''
				return 'Z', {}
			elseif ( curTypeU == 'H' or curTypeU == 'V' ) and #points >= 1 then
				return curType, { tonumber(table.remove( points, 1 )) }
			elseif ( curTypeU == 'S' or curTypeU == 'Q' ) and #points >= 4 then
				return curType, {
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 ))
				}
			elseif ( curTypeU == 'C' ) and #points >= 6 then
				return curType, {
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 ))
				}
			elseif ( curTypeU == 'A' ) and #points >= 7 then
				return curType, {
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 )),
					tonumber(table.remove( points, 1 ))
				}
			end
			-- We need to get the next entry.
			local pointsString
			curType, pointsString = getNextEntry()
			if curType == nil then
				curType = ''
				return
			end
			-- TODO this isn't quite right. 5.3.4 is supposed to be 5.3 0.4
			points = mw.text.split( mw.text.trim(pointsString), "[%s,]+" )
		end
	end	
end

-- Normalize the path into just L and M commands
local function convertToLines(pathIt)
	local type, points
	local startX, startY = 0,0
	local curX, curY = 0, 0
	local it
	it = function()
		type, points = pathIt()

		-- For many of these commands, we never make them, but the user
		-- can specify using p.Path2d( '...' )
		-- TODO better handle zero length line segments.
		if type == nil then
			-- we are done
			return
		elseif type == 'L' and #points == 2 then
			if curX == points[#points-1] and curY == points[#points] then
				-- rm zero length lines
				return it()
			end
			curX, curY = points[#points-1], points[#points]
			return type, points
		elseif type == 'l' and #points == 2 then
			curX, curY = points[#points-1]+curX, points[#points]+curY
			return 'L', { curX, curY }
		elseif type == 'M' and #points == 2 then
			if curX == points[#points-1] and curY == points[#points] then
				-- moving zero doesn't count as a new subpath
				return it()
			end
			curX, curY = points[#points-1], points[#points]
			startX, startY = curX, curY
			return 'M', { curX, curY }
		elseif type == 'm' and #points == 2 then
			if points[#points-1] == 0 and points[#points] == 0 then
				-- moving zero doesn't count as a new subpath
				return it()
			end
			curX, curY = points[#points-1]+curX, points[#points]+curY
			startX, startY = curX, curY
			return 'M', { curX, curY }
		elseif (type == 'z' or type == 'Z') and #points == 0 then
			curX, curY = startX, startY
			return 'M', {startX, startY}
		elseif type == 'H' and #points == 1 then
			curX = points[1]
			return 'L', { curX, curY }
		elseif type == 'h' and #points == 1 then
			curX = points[1] + curX
			return 'L', { curX, curY }
		elseif type == 'V' and #points == 1 then
			curY = points[1]
			return 'L', { curX, curY }
		elseif type == 'v' and #points == 1 then
			curY = points[1] + curX
			return 'L', { curX, curY }
		else
			-- TODO q s c t a
			error( "Either wrong number of points, or command " .. type .. " is not yet supported for stroking" )
		end
	end
	return it
end

-- Convert line segment into multiple line segments if we are drawing a dashed line
local function doDashes(ctx, pathIt)
	assert( type(ctx) == 'table' and type( pathIt ) == 'function' )
	local patternWidth = 0
	for i,v in ipairs( ctx._lineDash ) do
		patternWidth = patternWidth + v
	end
	if patternWidth == 0 then
		return function()
			return pathIt()
		end
	end
	local curSegmentPoints = nil
	local pathType = nil
	local curX, curY = 0, 0
	local endX, endY = 0, 0
	local dashOffset = ((ctx.lineDashOffset % patternWidth ) + patternWidth) % patternWidth
	local position = 0 - dashOffset
	local index = 0 -- note, array is 1-indexed
	local on = true
	local angle = nil
	local position = 0
	local subpathLen = 0
	local curDash = nil
	it = function ()
		if curSegmentPoints == nil or pathType == 'M' or position >= subpathLen  then
			repeat
				pathType, curSegmentPoints = pathIt()
				if curSegmentPoints == nil then
					-- all done
					return
				end

				if pathType == 'M' then
					-- new subpath
					curX, curY = curSegmentPoints[1], curSegmentPoints[2]
					dashOffset = ((ctx.lineDashOffset % patternWidth ) + patternWidth) % patternWidth
					position = 0 - dashOffset
					index = 0
					curDash = ctx._lineDash[index+1]
					return pathType, curSegmentPoints
				else
					assert( pathType == 'L' )
					assert( #curSegmentPoints == 2, "Expected 2 points. got " .. pathType .. ' with ' .. #curSegmentPoints )
					endX, endY = curSegmentPoints[1], curSegmentPoints[2]
					subpathLen = math.sqrt( (endX-curX)*(endX-curX) + (endY-curY)*(endY-curY) )
					angle = math.atan2((endY-curY),(endX-curX))
					if position > 0 then
						-- We want to reset position if we are drawing a new line in same subpath, but not for new subpath
						position = 0
					end
				end
			until pathType == 'L'
		end
		segmentLen = curDash
		assert( type( segmentLen) == 'number', "lineDash " .. index .. "+1 is not a number"  )

		local effectiveDashLen
		local curOn = on -- We want the change to on variable to apply next round not this round.
		if position < 0 and position+segmentLen > 0 then
			effectiveDashLen = -position
			curDash = curDash-effectiveDashLen
		elseif position+segmentLen <= subpathLen then
			effectiveDashLen = segmentLen
			index = (index+1) % #ctx._lineDash
			curDash = ctx._lineDash[index+1]
			on = not on
		else
			effectiveDashLen = subpathLen-position
			curDash = curDash-effectiveDashLen
		end
		dashOffset = dashOffset + effectiveDashLen

		position = position+effectiveDashLen
		local dashEndXRel = math.cos(angle)*effectiveDashLen
		local dashEndYRel = math.sin(angle)*effectiveDashLen
		-- The dash end before the current subpath ends.
		if position > 0 then
			curX = dashEndXRel+curX
			curY = dashEndYRel+curY
		end

		if curOn and position > 0 then
			return 'L', { curX, curY }
		else
			return 'M', { curX, curY }
		end
	end
	return it
end


-- 	for type, pointsCombined in string.gmatch( ctx._path, "(%a)%s*([0-9. ]+)" ) do repeat
--		points = mw.text.split( mw.text.trim(pointsCombined), "%s+" )

-- Not properly implemented. Only works on straight lines.
methodtable.stroke = function( ctx, path )
	ctx:save()
	if type( path ) == 'table' and type( path._path ) == 'string' then
		ctx._path = path._path
	elseif path ~= nil then
		error( "Invalid second argument to stroke" )
	end

	local newPath = p.Path2D()
	local curX = 0
	local curY = 0
	local startX = 0
	local startY = 0
	local offset = ctx.lineWidth/2
	local deg90 = math.pi/2
	local lastAngle = nil
	local startAngle = nil

	-- When drawing, it is important that we always draw in a clockwise direction
	-- per https://www.w3.org/TR/SVG2/painting.html#WindingRule clockwise and anti-clockwise
	-- cancel each other out.
	local draw = function( path, curX, newX, curY, newY )
		local angle = math.atan2((newY-curY),(newX-curX))-deg90
		local ypt = math.sin(angle)*offset
		local xpt = math.cos(angle)*offset
		newPath:moveTo( curX+xpt, curY+ypt )
		newPath:lineTo( newX+xpt, newY+ypt )
		newPath:lineTo( newX-xpt, newY-ypt )
		newPath:lineTo( curX-xpt, curY-ypt )
		newPath:lineTo( curX+xpt, curY+ypt )
		return angle+deg90
	end

	local function drawLineJoin( prevAngle, nextAngle, curX, curY )
		if prevAngle == newAngle then
			return
		end
		local xOffsetPrev = math.cos(prevAngle+deg90)*offset
		local yOffsetPrev = math.sin(prevAngle+deg90)*offset
		local xOffsetNext = math.cos(nextAngle+deg90)*offset
		local yOffsetNext = math.sin(nextAngle+deg90)*offset

		-- is the angle facing inwards or outwards
		local diff = ((nextAngle-prevAngle)+math.pi*2) % (math.pi*2)
		local xNextPoint, yNextPoint, xPrevPoint, yPrevPoint
		if diff > math.pi  then
			xNextPoint, yNextPoint = curX+xOffsetNext, curY+yOffsetNext
			xPrevPoint, yPrevPoint = curX+xOffsetPrev, curY+yOffsetPrev
		else
			xNextPoint, yNextPoint = curX-xOffsetNext, curY-yOffsetNext
			xPrevPoint, yPrevPoint = curX-xOffsetPrev, curY-yOffsetPrev
		end

		newPath:moveTo( curX, curY )
		newPath:lineTo( xNextPoint, yNextPoint )
		newPath:lineTo( xPrevPoint, yPrevPoint )
		newPath:closePath()

		if ctx.lineJoin == 'round' then
			if diff > math.pi then
				newPath:moveTo( xNextPoint, yNextPoint )
				newPath:addPath( p.Path2D( string.format(
					"A %.8g %.8g 0 0 1 %.8g %.8g",
					r0(offset),
					r0(offset),
					r0(xPrevPoint),
					r0(yPrevPoint)
				)))
			else
				newPath:moveTo( xPrevPoint, yPrevPoint )
				newPath:addPath( p.Path2D( string.format(
					"A %.8g %.8g 0 0 1 %.8g %.8g",
					r0(offset),
					r0(offset),
					r0(xNextPoint),
					r0(yNextPoint)
				)))
			end
		elseif ctx.lineJoin == 'miter' then
			-- We have to determine where the intersection point is
			local prevIntercept = yPrevPoint-math.tan(prevAngle)*xPrevPoint
			local nextIntercept = yNextPoint-math.tan(nextAngle)*xNextPoint
			local xIntersect = (nextIntercept-prevIntercept)/(math.tan(prevAngle)-math.tan(nextAngle))
			local yIntersect = math.tan(prevAngle)*xIntersect + prevIntercept
			local maxMiter = ctx.miterLimit*offset
			-- FIXME I'm a bit confused by the definition of miter length in spec, so not
			-- sure if this is right.
			if math.sqrt( (curX-xIntersect)*(curX-xIntersect)+(curY-yIntersect)*(curY-yIntersect) ) < maxMiter then
				if xIntersect > xPrevPoint then
					newPath:moveTo( xPrevPoint, yPrevPoint )
					newPath:lineTo( xNextPoint, yNextPoint )
					newPath:lineTo( xIntersect, yIntersect )
					newPath:closePath()
				else
					newPath:moveTo( xNextPoint, yNextPoint )
					newPath:lineTo( xPrevPoint, yPrevPoint )
					newPath:lineTo( xIntersect, yIntersect )
					newPath:closePath()
				end
			end
		end
	end

	local function drawLineCap( newPath, lineCap, angle, curX, curY )
		if angle == nil then
			mw.log( "nil angle. Possibly a bug in canvas" )
			return
		end

		if lineCap == 'butt' then
			-- do nothing
			return
		elseif lineCap == 'square' then
			local endPointX = curX + math.cos(angle)*offset
			local endPointY = curY + math.sin(angle)*offset
			draw( newPath, curX, endPointX, curY, endPointY )
		elseif lineCap == 'round' then
			local startPtX = curX + math.cos(angle+deg90)*offset
			local startPtY = curY + math.sin(angle+deg90)*offset
			local endPtX = curX - math.cos(angle+deg90)*offset
			local endPtY = curY - math.sin(angle+deg90)*offset

			newPath:moveTo( endPtX, endPtY )
			-- A rx ry x-axis-rotation large-arc-flag sweep-flag(clockwise) x y
			newPath:addPath( p.Path2D( string.format(
				"A %.8g %.8g 0 0 1 %.8g %.8g",
				r0(offset),
				r0(offset),
				r0(startPtX),
				r0(startPtY)
			)))
		else
			error( "Unrecognized lineCap of " .. ctx.lineCap )
		end
	end

	-- Note, its important we always draw clockwise, as CCW can create holes
	for type, points in doDashes( ctx, convertToLines( parsePath( ctx._path ) ) ) do repeat
		assert( #points == 2, "expected 2 points")
		assert( _G.type(points[1]) == 'number' and _G.type(points[2]) == 'number', "Expected points to be numbers" )
		if curX-points[#points-1] == 0 and curY-points[#points] == 0 then
			-- Zero-length line. Skip
			break
		end
		if type == 'L' then
			-- This is probably doing it totally wrong way.

			local prevAngle = lastAngle
			lastAngle = draw( newPath, curX, points[1], curY, points[2] )
			assert(  _G.type( lastAngle ) == 'number', 'expected last angle to be a number' )
			if startAngle == nil then
				startAngle = lastAngle + math.pi
			else
				-- Having a previous startAngle means that we are drawing a line
				-- that connects to a previous line, so we have to join it.
				-- TODO this seems to get incorrectly called on first line of subpath.
				drawLineJoin( prevAngle, lastAngle, curX, curY )
			end
			curX = points[1]
			curY = points[2]
		elseif type == 'M'  then
			if curX ~= startX or curY ~= startY then
				-- We are at the end of a subpath so need to draw a line ending
				drawLineCap( newPath, ctx.lineCap, lastAngle, curX, curY )
				drawLineCap( newPath, ctx.lineCap, startAngle, startX, startY )
			elseif startAngle ~= nil then
				-- Closed path and we drew at least one thing.
				drawLineJoin( startAngle, lastAngle, curX, curY )
			end
			angle = nil
			curX = points[1]
			curY = points[2]
			startX = points[1]
			startY = points[2]
			startAngle = nil
		else
			-- Should be impossible to reach
			error( "Unexpected path command" )
		end
	until true end
	-- draw caps for final line segment.
	if curX ~= startX or curY ~= startY then
		-- We are at the end of a subpath so need to draw a line ending
		drawLineCap( newPath, ctx.lineCap, lastAngle, curX, curY )
		drawLineCap( newPath, ctx.lineCap, startAngle, startX, startY )
	elseif startAngle ~= nil then
		-- Closed path and we drew at least one thing.
		drawLineJoin( startAngle, (lastAngle+math.pi) % (math.pi*2), curX, curY )
	end
	ctx.fillStyle = ctx.strokeStyle
	ctx:fill(newPath)

	ctx:restore()
end

methodtable.createWikitextPattern = function( ctx, args )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	if type( args ) == 'string' then
		args = { background = args }
	end
	local res = {
		background =  args.background or 'transparent',
		class = args.class or nill,
		style = args.style or nill,
		content = args.content or '', -- should this be parsed?
		offsetx = args.offsetx or 0, -- FIXME this should be removed ??
		offsety = args.offsety or 0,
		attr = args.attr or nil
	}
	return res
end

methodtable.drawImage = function( ctx, image, sx, sy, sw, sh, dx, dy, dw, dh )
	-- FIXME this doesn't work properly yet
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	if image == nil then
		return
	end
	if type( image ) == 'string' then
		image = mw.title.new( image, 6 )
	end
	if ( not image:inNamespace( 6 ) ) or (not image.file.exists) then
		return
	end
	if sx == nil then
		sx = 0
	end
	if sy == nil then
		sx = 0
	end
	if sw == nil then
		sw = image.file.width
	end
	if sh == nil then
		sh = image.file.height
	end
	if dx == nil then
		dx = sx
	end
	if dy == nil then
		dy = sy
	end
	if dw == nil then
		dw = sw
	end
	if dh == nil then
		dh = sh
	end

	ctx:save()
	-- FIXME, this is broken and doesn't work right for all arg types
	local img
	if image.file.width > image.file.height then
		img = '[[File:' .. image.text .. '|' .. dw .. 'px' .. '|link=]]'
	else
		img = '[[File:' .. image.text .. '|x' .. dh .. 'px' .. '|link=]]'
	end
	-- FIXME doesn't work with negative values properly
	local clip = 'path("M ' .. sx .. ' ' .. sy .. ' L ' .. (sw+sx) .. ' ' .. sy .. ' L ' .. (sw+sx) .. ' ' .. (sh+sy) .. ' L ' .. sx .. ' ' .. (sh+sy) ..' )'
	-- Note in timeless, if the image is linked, then there are css rules that resize it which we don't want.
	img = '<div style="position:relative;left:' .. (-sx) .. 'px;top:' .. (-sy) .. 'px;clip-path:' .. clip .. '">' .. img .. '</div>'
	ctx.fillStyle = ctx:createWikitextPattern{
		--offsetx = dx,
		offsetx = 0,
		offsety = 0,
		--offsety = dy,
		content = img
	}
	ctx:fillRect( dx, dy, dw, dh )
	ctx:restore()

end

isFinite = function( n )
	return n > -math.huge and n < math.huge	
end


methodtable.fillRect = function( ctx, x, y, w, h )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	if not isFinite( x ) or not isFinite( y ) or not isFinite( w ) or not isFinite( h ) or w == 0 or h == 0 then
		return
	end
	local oldPath = ctx._path
	ctx:beginPath()
	ctx:rect( x, y, w, h )
	ctx:fill()
	ctx:beginPath()
	ctx._path = oldPath
end

methodtable.clearRect = function( ctx, x, y, w, h )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	-- FXIME, this should make it transparent to html below canvas, not just be white.
	ctx:save()
	ctx.fillStyle = "var(--background-color-base, '#fff')"
	ctx:fillRect( x, y, w, h )
	ctx:restore()
end

local doText = function( ctx, text, x, y, maxWidth, stroke )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	assert( maxWidth == nil, "maxWidth parameter to fillText is not supported" )
	ctx:save()
	local oldStyle = stroke and ctx.strokeStyle or ctx.fillStyle
	-- Maybe complex background is possible here with mix-blend-mode: screen. Main issue is it seems like
	-- we couldn't make the non-text part be transparent.
	assert( type(oldStyle) == 'string', 'Complex backgrounds for fillText() not currently supported' )
	text = string.gsub( text, "[\n\r\t\f]", " " ) -- per spec, replace whitespace (Note: we use white-space: pre css)

	local textLayer = mw.html.create( 'div' )
		:css( 'position', 'absolute' )
		:css( 'width', 'max-content' )
		:css( 'font', ctx.font )
		:css( 'text-rendering', ctx.textRendering )
		:css( 'font-kerning', ctx.fontKerning )
		:css( 'font-stretch', ctx.fontStretch )
		:css( 'font-variant-caps', ctx.fontVariantCaps )
		:css( 'letter-spacing', ctx.letterSpacing )
		:css( 'word-spacing', ctx.wordSpacing )
		:css( 'text-align', 'left' )
		:css( 'white-space', 'pre' )
		:wikitext( text ) -- FIXME should we escape

	if ctx.direction ~= 'inherit' then
		textLayer:attr( 'dir', ctx.direction )
	end
	if ctx.textBaseline == 'alphabetic' or ctx.textBaseline == 'bottom' then
		-- This isn't 100% right for alphabetic, but it is the default and this is close
		textLayer:css( 'bottom', 'calc( 100% - ' .. y .. 'px' .. ' )' )
	elseif ctx.textBaseline == 'top' then
		textLayer:css( 'top', y .. 'px' )
	else
		-- We can approximate some values, but better to just give an error.
		error( "Unsupported value for textBaseline: " .. ctx.textBaseline )
	end

	-- not perfect, as its supposed to inherit from containing element
	local realDir = ctx.direction == 'inherit' and mw.getContentLanguage():getDir() or ctx.direction
	local realAlign = 'left'
	if ctx.textAlign == 'start' and realDir == 'rtl' then
		realAlign = 'right'
	elseif ctx.textAlign == 'end' and realDir == 'ltr' then
		realAlign = 'right'
	end
	if ctx.textAlign == 'center' then
		textLayer:css( 'width', ctx._width .. 'px' )
		textLayer:css( 'left', (x-ctx._width)/2 .. 'px' )
		textLayer:css( 'text-align', 'center' )
	elseif realAlign == 'left' then
		textLayer:css( 'left', x .. 'px' )
	else
		textLayer:css( 'right', 'calc( 100% - ' .. x .. 'px )' )
	end

	local style = 'color:' .. oldStyle
	if stroke then
		style = 'color: transparent; -webkit-text-stroke-color: ' .. oldStyle .. ';text-stroke-color:' .. oldStyle ..
			'; -webkit-text-stroke-width:' .. ctx.lineWidth .. 'px; text-stroke-width:' .. ctx.lineWidth .. 'px;'
	end
	ctx.fillStyle = ctx:createWikitextPattern{
		style = style,
		content = textLayer
	}
	local op = newOperation( ctx, 'text' )
	table.insert( ctx.__operations, op )
	ctx:restore()
end


methodtable.fillText = function( ctx, text, x, y, maxWidth )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	ctx:fillTextRaw( mw.text.nowiki( text ), x, y, maxWidth )
end

-- For use if you want to include wikitext. Note you still need to use frame:preprocess before calling this.
methodtable.fillTextRaw = function( ctx, text, x, y, maxWidth )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	doText( ctx, text, x, y, maxWidth, false )
end

-- For use if you want to include wikitext. Note you still need to use frame:preprocess before calling this.
methodtable.strokeTextRaw = function( ctx, text, x, y, maxWidth )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	doText( ctx, text, x, y, maxWidth, true )
end
methodtable.strokeText = function( ctx, text, x, y, maxWidth )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	ctx:strokeTextRaw( mw.text.nowiki( text ), x, y, maxWidth )
end

methodtable.save = function (ctx)
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	-- Note, path is included in operation, however it is not part of save state.
	table.insert( ctx.__stateStack, newOperation( ctx, 'save' ) )
end

methodtable.restore = function(ctx)
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	if #ctx.__stateStack == 0 then
		-- spec says silently ignore if no saved state
		return
	end
	
	op = table.remove( ctx.__stateStack )

	ctx._currentTransform = op._currentTransform
	ctx.lineWidth = op.lineWidth 
	ctx.lineCap = op.lineCap 
	ctx.lineJoin = op.lineJoin 
	ctx.miterLimit = op.miterLimit 
	ctx.lineDashOffset = op.lineDashOffset 
	ctx.font = op.font 
	ctx.textAlign = op.textAlign 
	ctx.textBaseline = op.textBaseline 
	ctx.direction = op.direction 
	ctx.letterSpacing = op.letterSpacing 
	ctx.fontKerning = op.fontKerning 
	ctx.fontStretch = op.fontStretch 
	ctx.fontVariantCaps = op.fontVariantCaps 
	ctx.textRendering = op.textRendering 
	ctx.wordSpacing = op.wordSpacing 
	ctx.fillStyle = op.fillStyle 
	ctx.strokeStyle = op.strokeStyle 
	ctx.shadowBlur = op.shadowBlur 
	ctx.shadowColor = op.shadowColor 
	ctx.shadowOffsetX = op.shadowOffsetX 
	ctx.shadowOffsetY = op.shadowOffsetY 
	ctx.globalAlpha = op.globalAlpha 
	ctx.globalCompositeOperation = op.globalCompositeOperation 
	ctx.imageSmoothingEnabled = op.imageSmoothingEnabled 
	ctx.imageSmoothingQuality = op.imageSmoothingQuality 
	ctx.canvas = op.canvas 
	ctx.filter = op.filter
end

methodtable.reset = function (ctx)
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	setDefaults( ctx )
end


methodtable.isContextLost = function( t )
	return false
end

methodtable.getContextAttributes = function( ctx )
	assert( type(ctx) == 'table' and type(ctx.__stateStack) == 'table', "First argument must be a CanvasRenderingContext2D. Did you use '.' instead of ':'?" )
	return {
		width = ctx._width,
		height = ctx._height,
		containerClass = ctx._containerClass,
		containerStyle = ctx._containerStyle,
		alpha = ctx._alpha
	}
end

local getBlendMode = function ( compositeOp )
	-- css mix-blend-mode only supports blending not composite operators (except plus-darker and plus-lighter)
	-- So we don't support the following values: clear | copy | source-over | destination-over | source-in |    
	-- destination-in | source-out | destination-out | source-atop |    
	-- destination-atop | xor | lighter

	-- Also this doesn't work for images properly as images have isolated blend modes.
	validOps = {
		normal = true,
		multiply = true,
		screen = true,
		overlay = true,
		darken = true,
		lighten = true,
		["color-dodge"] = true,
		["color-burn"] = true,
		["hard-light"] = true,
		["soft-light"] = true,
		difference = true,
		exclusion = true,
		hue = true,
		saturation = true,
		color = true,
		luminosity = true,
		['plus-darker'] = true,
		['plus-lighter'] = true,
	}
	if validOps[compositeOp] then
		return compositeOp
	end
	return "normal"
end

local getTransform = function( t )
	local res = 'matrix(' .. t[1] .. ',' .. t[2] .. ',' .. t[3] .. ',' .. t[4] .. ',' .. t[5] .. ',' .. t[6] .. ')'
	if res == 'matrix(1,0,0,1,0,0)' then
		return 'none'
	end
	return res
end

-- TODO this is a hack that doesn't really work.
local getAdjustedWidth = function( w, h, t )
	--  ( a x + c y + e , b x + d y + f ) 
	-- we should really invert the matrix instead of this hack
	return math.abs(math.ceil(w*w/(w*t[1]+h*t[3])+math.abs(t[5])))
end

local getAdjustedHeight = function( w, h, t )
	--  ( a x + c y + e , b x + d y + f ) 
	-- we should really invert the matrix instead of this hack
	return math.abs(math.ceil(h*h/(w*t[2]+h*t[4])+math.abs(t[6])))
end

local getFilter = function( op )
	if op.shadowColor == 'transparent' then
		if op.filter == 'none' then
			return nil
		end
		return op.filter
	end
	local shadow = " drop-shadow(" .. op.shadowColor .. ' ' .. op.shadowOffsetX .. 'px '
		.. op.shadowOffsetY .. 'px ' .. op.shadowBlur .. 'px)'
	if op.filter == 'none' then
		return shadow
	end
	return op.filter .. shadow
end

--[[


]]
methodtable.getWikitext = function( ctx )
	local container = mw.html.create( 'div' )
	container:attr( "role", 'presentation' )
		:attr( "aria-hidden", "true" ) -- not sure if this is right
		:css( 'width', ctx._width .. 'px' )
		:css( 'height', ctx._height .. 'px' )
		:css( 'overflow', 'hidden' )
		:css( 'position', 'relative' )
		:cssText( ctx._containerStyle )
		:addClass( ctx._containerClass )

	if ctx._alpha == false then
		container:css( 'isolation', 'isolate' )
	end

	local layers = ''
	for i, op in ipairs( ctx.__operations) do
		if op['name'] == 'fill' or op['name'] == 'text' then
				local fillPattern = ctx:createWikitextPattern( op.fillStyle )
				local layer = mw.html.create( 'div' )
					:cssText( fillPattern.style )
					:addClass( fillPattern.class )
					:attr( fillPattern.attr or {} ) -- FIXME should class and attr be set on inner div instead?
					:css( 'width', getAdjustedWidth( ctx._width, ctx._height, op._currentTransform ) .. 'px' )  -- Should this be adjusted based on fillPattern?
					:css( 'height', getAdjustedHeight( ctx._width, ctx._height, op._currentTransform ) .. 'px' )
					:css( 'left', fillPattern.offsetx ) -- FIXME i think this is wrong
					:css( 'top', fillPattern.offsety )
					:css( 'position', 'absolute' )
					:css( 'filter', getFilter(op) )
					:css( 'mix-blend-mode', getBlendMode(op.globalCompositeOperation))
					:css( 'opacity', op.globalAlpha )
					:css( 'transform', getTransform( op._currentTransform ) )
					:css( 'transform-origin', 'top left' )
					:css( 'pointer-events', 'none' ) -- Make sure we pass :hover to layer below
						:tag( 'div' )
							:css( 'width', '100%' )
							:css( 'height', '100%' )
							-- FIXME this isn't really right. Clear should be transparent to the non-canvas background
							:css( 'background-color', op.globalCompositeOperation == 'clear' and "var(--background-color-base, '#fff')" or fillPattern.background )
							:css( 'color', 'inherit' ) -- Hack for night mode
							:css( 'clip-path', op['name'] == 'fill' and 'path(' .. op._fillRule .. ', \'' .. op._path .. '\')' or 'none' )
							:css( 'pointer-events', 'all' )
							:wikitext( tostring(fillPattern.content) )
							:allDone()
			if op.globalCompositeOperation == "source-over"
				or op.globalCompositeOperation == 'clear'
				or op.globalCompositeOperation == 'normal'
				or getBlendMode(op.globalCompositeOperation) ~= 'normal'
			then
				layers = layers .. tostring( layer )
			elseif op.globalCompositeOperation == 'destination-over' then
				layers = tostring( layer ) .. layers
			else
				error( "Unsupported globalCompositeOperation " .. op.globalCompositeOperation )
			end
		else
			error( "unsupported operation " .. v['name'] )
		end
	end
	container:wikitext( layers )
	return tostring( container )
end



return p