void EJCanvasContext::pushQuad(EJVector2 v1, EJVector2 v2, EJVector2 v3, EJVector2 v4, EJVector2 t1, EJVector2 t2, EJVector2 t3, EJVector2 t4, EJColorRGBA color, CGAffineTransform transform) { if( vertexBufferIndex >= EJ_CANVAS_VERTEX_BUFFER_SIZE - 6 ) { flushBuffers(); } if( !CGAffineTransformIsIdentity(transform) ) { v1 = EJVector2ApplyTransform( v1, transform ); v2 = EJVector2ApplyTransform( v2, transform ); v3 = EJVector2ApplyTransform( v3, transform ); v4 = EJVector2ApplyTransform( v4, transform ); } EJVertex * vb = &CanvasVertexBuffer[vertexBufferIndex]; EJVertex vb_0 = { v1, t1, color }; EJVertex vb_1 = { v2, t2, color }; EJVertex vb_2 = { v3, t3, color }; EJVertex vb_3 = { v2, t2, color }; EJVertex vb_4 = { v3, t3, color }; EJVertex vb_5 = { v4, t4, color }; vb[0] = vb_0; vb[1] = vb_1; vb[2] = vb_2; vb[3] = vb_3; vb[4] = vb_4; vb[5] = vb_5; vertexBufferIndex += 6; }
void EJCanvasContext::pushTri(float x1, float y1, float x2, float y2, float x3, float y3, EJColorRGBA color, CGAffineTransform transform) { if( vertexBufferIndex >= EJ_CANVAS_VERTEX_BUFFER_SIZE - 3 ) { flushBuffers(); } EJVector2 d1 = { x1, y1 }; EJVector2 d2 = { x2, y2 }; EJVector2 d3 = { x3, y3 }; if( !CGAffineTransformIsIdentity(transform) ) { d1 = EJVector2ApplyTransform( d1, transform ); d2 = EJVector2ApplyTransform( d2, transform ); d3 = EJVector2ApplyTransform( d3, transform ); } EJVertex * vb = &CanvasVertexBuffer[vertexBufferIndex]; EJVertex vb_0 = {d1, {0.5, 1}, color}; EJVertex vb_1 = { d2, {0.5, 0.5}, color }; EJVertex vb_2 = { d3, {0.5, 1}, color }; vb[0] = vb_0; vb[1] = vb_1; vb[2] = vb_2; vertexBufferIndex += 3; }
void EJCanvasContext::pushRect(float x, float y, float w, float h, float tx, float ty, float tw, float th, EJColorRGBA color, CGAffineTransform transform) { if( vertexBufferIndex >= EJ_CANVAS_VERTEX_BUFFER_SIZE - 6 ) { flushBuffers(); } EJVector2 d11 = { x, y }; EJVector2 d21 = { x+w, y }; EJVector2 d12 = { x, y+h }; EJVector2 d22 = { x+w, y+h }; if( !CGAffineTransformIsIdentity(transform) ) { d11 = EJVector2ApplyTransform( d11, transform ); d21 = EJVector2ApplyTransform( d21, transform ); d12 = EJVector2ApplyTransform( d12, transform ); d22 = EJVector2ApplyTransform( d22, transform ); } EJVertex * vb = &CanvasVertexBuffer[vertexBufferIndex]; EJVertex vb_0 = { d11, {tx, ty}, color }; // top left EJVertex vb_1 = { d21, {tx+tw, ty}, color }; // top right EJVertex vb_2 = { d12, {tx, ty+th}, color }; // bottom left EJVertex vb_3 = { d21, {tx+tw, ty}, color }; // top right EJVertex vb_4 = { d12, {tx, ty+th}, color }; // bottom left EJVertex vb_5 = { d22, {tx+tw, ty+th}, color };// bottom right vb[0] = vb_0; // top left vb[1] = vb_1; // top right vb[2] = vb_2; // bottom left vb[3] = vb_3; // top right vb[4] = vb_4; // bottom left vb[5] = vb_5;// bottom right vertexBufferIndex += 6; }
FloatRect GraphicsContext::roundToDevicePixels(const FloatRect& rect) { // It is not enough just to round to pixels in device space. The rotation part of the // affine transform matrix to device space can mess with this conversion if we have a // rotating image like the hands of the world clock widget. We just need the scale, so // we get the affine transform matrix and extract the scale. if (m_data->m_userToDeviceTransformKnownToBeIdentity) return rect; CGAffineTransform deviceMatrix = CGContextGetUserSpaceToDeviceSpaceTransform(platformContext()); if (CGAffineTransformIsIdentity(deviceMatrix)) { m_data->m_userToDeviceTransformKnownToBeIdentity = true; return rect; } float deviceScaleX = sqrtf(deviceMatrix.a * deviceMatrix.a + deviceMatrix.b * deviceMatrix.b); float deviceScaleY = sqrtf(deviceMatrix.c * deviceMatrix.c + deviceMatrix.d * deviceMatrix.d); CGPoint deviceOrigin = CGPointMake(rect.x() * deviceScaleX, rect.y() * deviceScaleY); CGPoint deviceLowerRight = CGPointMake((rect.x() + rect.width()) * deviceScaleX, (rect.y() + rect.height()) * deviceScaleY); deviceOrigin.x = roundf(deviceOrigin.x); deviceOrigin.y = roundf(deviceOrigin.y); deviceLowerRight.x = roundf(deviceLowerRight.x); deviceLowerRight.y = roundf(deviceLowerRight.y); // Don't let the height or width round to 0 unless either was originally 0 if (deviceOrigin.y == deviceLowerRight.y && rect.height()) deviceLowerRight.y += 1; if (deviceOrigin.x == deviceLowerRight.x && rect.width()) deviceLowerRight.x += 1; FloatPoint roundedOrigin = FloatPoint(deviceOrigin.x / deviceScaleX, deviceOrigin.y / deviceScaleY); FloatPoint roundedLowerRight = FloatPoint(deviceLowerRight.x / deviceScaleX, deviceLowerRight.y / deviceScaleY); return FloatRect(roundedOrigin, roundedLowerRight - roundedOrigin); }
bool AffineTransform::isIdentity() const { return CGAffineTransformIsIdentity(m_transform); }
void EJPath::drawLinesToContext(EJCanvasContext * context) { endSubPath(); EJCanvasState * state = context->state; EJVector2 vecZero = { 0.0f, 0.0f }; // Find the width of the line as it is projected onto the screen. float projectedLineWidth = CGAffineTransformGetScale( state->transform ) * state->lineWidth; context->setTexture(NULL); // Figure out if we need to add line caps and set the cap texture coord for square or round caps. // For thin lines we disable texturing and line caps. float width2 = state->lineWidth/2; BOOL addCaps = (projectedLineWidth > 2 && (state->lineCap == kEJLineCapRound || state->lineCap == kEJLineCapSquare)); // The miter limit is the maximum allowed ratio of the miter length to half the line width. BOOL addMiter = (state->lineJoin == kEJLineJoinMiter); float miterLimit = (state->miterLimit * width2); EJColorRGBA color = state->strokeColor; color.rgba.a = (unsigned char)(color.rgba.a * state->globalAlpha); // Enable stencil test when drawing transparent lines. // Cycle through all bits, so that the stencil buffer only has to be cleared after eight stroke operations if( color.rgba.a < 0xff ) { context->flushBuffers(); context->createStencilBufferOnce(); glEnable(GL_STENCIL_TEST); glStencilMask(stencilMask); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); glStencilFunc(GL_NOTEQUAL, stencilMask, stencilMask); } // To draw the line correctly with transformations, we need to construct the line // vertices from the untransformed points and only apply the transformation in // the last step (pushQuad) again. CGAffineTransform inverseTransform = CGAffineTransformIsIdentity(transform) ? transform : CGAffineTransformInvert(transform); // Oh god, I'm so sorry... This code sucks quite a bit. I'd be surprised if I // will understand what I've written in 3 days :/ // Calculating line miters for potentially closed paths is serious business! // And it doesn't even handle all the edge cases. EJVector2 *transCurrent, *transNext, // Pointers to current and next vertices on the line current, next, // Untransformed current and next points firstMiter1, firstMiter2, // First miter vertices (left, right) needed for closed paths miter11, miter12, // Current miter vertices (left, right) miter21, miter22, // Next miter vertices (left, right) currentEdge, currentExt, // Current edge and its normal * width/2 nextEdge = { 0.0f, 0.0f }, nextExt = { 0.0f, 0.0f }; // Next edge and its normal * width/2 for( path_t::iterator sp = paths.begin(); sp != paths.end(); ++sp ) { BOOL subPathIsClosed = sp->isClosed; BOOL ignoreFirstSegment = addMiter && subPathIsClosed; BOOL firstInSubPath = true; BOOL miterLimitExceeded = false, firstMiterLimitExceeded = false; transCurrent = transNext = NULL; // If this subpath is closed, initialize the first vertex for the loop ("next") // to the last vertex in the subpath. This way, the miter between the last and // the first segment will be computed and used to draw the first segment's first // miter, as well as the last segment's last miter outside the loop. if( addMiter && subPathIsClosed ) { transNext = &sp->points.at(sp->points.size()-2); next = EJVector2ApplyTransform( *transNext, inverseTransform ); } for( points_t::iterator vertex = sp->points.begin(); vertex != sp->points.end(); ++vertex) { transCurrent = transNext; transNext = &(*vertex); current = next; next = EJVector2ApplyTransform( *transNext, inverseTransform ); if( !transCurrent ) { continue; } currentEdge = nextEdge; currentExt = nextExt; nextEdge = EJVector2Normalize(EJVector2Sub(next, current)); nextExt = EJVector2Make( -nextEdge.y * width2, nextEdge.x * width2 ); if( firstInSubPath ) { firstMiter1 = miter21 = EJVector2Add( current, nextExt ); firstMiter2 = miter22 = EJVector2Sub( current, nextExt ); firstInSubPath = false; // Start cap if( addCaps && !subPathIsClosed ) { if( state->lineCap == kEJLineCapSquare ) { EJVector2 capExt = { -nextExt.y, nextExt.x }; EJVector2 cap11 = EJVector2Add( miter21, capExt ); EJVector2 cap12 = EJVector2Add( miter22, capExt ); context-> pushQuad(cap11 ,cap12 ,miter21 ,miter22 ,vecZero ,vecZero ,vecZero ,vecZero ,color ,transform); } else { drawArcToContext(context, current, miter22, miter21, color); } } continue; } miter11 = miter21; miter12 = miter22; BOOL miterAdded = false; if( addMiter ) { EJVector2 miterEdge = EJVector2Add( currentEdge, nextEdge ); float miterExt = (1/EJVector2Dot(miterEdge, miterEdge)) * state->lineWidth; if( miterExt < miterLimit ) { miterEdge.x *= miterExt; miterEdge.y *= miterExt; miter21 = EJVector2Make( current.x - miterEdge.y, current.y + miterEdge.x ); miter22 = EJVector2Make( current.x + miterEdge.y, current.y - miterEdge.x ); miterAdded = true; miterLimitExceeded = false; } else { miterLimitExceeded = true; } } // No miter added? Calculate the butt for the current segment if( !miterAdded ) { miter21 = EJVector2Add(current, currentExt); miter22 = EJVector2Sub(current, currentExt); } if( ignoreFirstSegment ) { // True when starting from the back vertex of a closed path. This run was just // to calculate the first miter. firstMiter1 = miter21; firstMiter2 = miter22; if( !miterAdded ) { // Flip miter21 <> miter22 if it's the butt for the first segment miter21 = firstMiter2; miter22 = firstMiter1; } firstMiterLimitExceeded = miterLimitExceeded; ignoreFirstSegment = false; continue; } if( !addMiter || miterLimitExceeded ) { // previous point can be approximated, good enough for distance comparison EJVector2 prev = EJVector2Sub(current, currentEdge); EJVector2 p1, p2; float d1, d2; // calculate points to use for bevel // two points are possible for each edge - the one farthest away from the other line has to be used // calculate point for current edge d1 = EJDistanceToLineSegmentSquared(miter21, current, next); d2 = EJDistanceToLineSegmentSquared(miter22, current, next); p1 = ( d1 > d2 ) ? miter21 : miter22; // calculate point for next edge d1 = EJDistanceToLineSegmentSquared(EJVector2Add(current, nextExt), current, prev); d2 = EJDistanceToLineSegmentSquared(EJVector2Sub(current, nextExt), current, prev); p2 = ( d1 > d2 ) ? EJVector2Add(current, nextExt) : EJVector2Sub(current, nextExt); if( state->lineJoin==kEJLineJoinRound ) { drawArcToContext(context, current, p1, p2, color); } else { context-> pushTri(p1.x ,p1.y ,current.x,current.y ,p2.x ,p2.y ,color ,transform); } } context-> pushQuad(miter11 ,miter12 ,miter21 ,miter22 ,vecZero ,vecZero ,vecZero ,vecZero ,color ,transform); // No miter added? The "miter" for the next segment needs to be the butt for the next segment, // not the butt for the current one. if( !miterAdded ) { miter21 = EJVector2Add(current, nextExt); miter22 = EJVector2Sub(current, nextExt); } } // for each subpath // The last segment, not handled in the loop if( !firstMiterLimitExceeded && addMiter && subPathIsClosed ) { miter11 = firstMiter1; miter12 = firstMiter2; } else { EJVector2 untransformedBack = EJVector2ApplyTransform(sp->points.back(), inverseTransform); miter11 = EJVector2Add(untransformedBack, nextExt); miter12 = EJVector2Sub(untransformedBack, nextExt); } if( (!addMiter || firstMiterLimitExceeded) && subPathIsClosed ) { float d1,d2; EJVector2 p1,p2, firstNormal = EJVector2Sub(firstMiter1,firstMiter2), // unnormalized line normal for first edge second = EJVector2Add(next,EJVector2Make(firstNormal.y,-firstNormal.x)); // approximated second point // calculate points to use for bevel // two points are possible for each edge - the one farthest away from the other line has to be used // calculate point for current edge d1 = EJDistanceToLineSegmentSquared(miter12, next, second); d2 = EJDistanceToLineSegmentSquared(miter11, next, second); p2 = ( d1 > d2 )?miter12:miter11; // calculate point for next edge d1 = EJDistanceToLineSegmentSquared(firstMiter1, current, next); d2 = EJDistanceToLineSegmentSquared(firstMiter2, current, next); p1 = (d1>d2)?firstMiter1:firstMiter2; if( state->lineJoin==kEJLineJoinRound ) { drawArcToContext(context, next, p1, p2, color); } else { context-> pushTri(p1.x ,p1.y ,next.x ,next.y ,p2.x,p2.y ,color ,transform); } } context-> pushQuad(miter11,miter12,miter21,miter22 ,vecZero ,vecZero ,vecZero ,vecZero ,color ,transform); // End cap if( addCaps && !subPathIsClosed ) { if( state->lineCap == kEJLineCapSquare ) { EJVector2 capExt = { nextExt.y, -nextExt.x }; EJVector2 cap11 = EJVector2Add( miter11, capExt ); EJVector2 cap12 = EJVector2Add( miter12, capExt ); context-> pushQuad(cap11,cap12,miter11,miter12 ,vecZero,vecZero,vecZero ,vecZero ,color ,transform); } else { drawArcToContext(context, next, miter11, miter12, color); } } } // for each path // disable stencil test when drawing transparent lines if( color.rgba.a < 0xff ) { context->flushBuffers(); glDisable(GL_STENCIL_TEST); if( stencilMask == (1<<7) ) { stencilMask = (1<<0); glStencilMask(0xff); glClearStencil(0x0); glClear(GL_STENCIL_BUFFER_BIT); } else { stencilMask <<= 1; } } }