/**************************************************************************** ** ** Copyright (C) 2017 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the examples of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:BSD$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** BSD License Usage ** Alternatively, you may use this file under the terms of the BSD license ** as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of The Qt Company Ltd nor the names of its ** contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "context2d.h" #include #include #define qClamp(val, min, max) qMin(qMax(val, min), max) static QList parseNumbersList(QString::const_iterator &itr) { QList points; QString temp; while ((*itr).isSpace()) ++itr; while ((*itr).isNumber() || (*itr) == '-' || (*itr) == '+' || (*itr) == '.') { temp = QString(); if ((*itr) == '-') temp += *itr++; else if ((*itr) == '+') temp += *itr++; while ((*itr).isDigit()) temp += *itr++; if ((*itr) == '.') temp += *itr++; while ((*itr).isDigit()) temp += *itr++; while ((*itr).isSpace()) ++itr; if ((*itr) == ',') ++itr; points.append(temp.toDouble()); //eat spaces while ((*itr).isSpace()) ++itr; } return points; } QColor colorFromString(const QString &name) { QString::const_iterator itr = name.constBegin(); QList compo; if (name.startsWith("rgba(")) { ++itr; ++itr; ++itr; ++itr; ++itr; compo = parseNumbersList(itr); if (compo.size() != 4) { return QColor(); } //alpha seems to be always between 0-1 compo[3] *= 255; return QColor((int)compo[0], (int)compo[1], (int)compo[2], (int)compo[3]); } else if (name.startsWith("rgb(")) { ++itr; ++itr; ++itr; ++itr; compo = parseNumbersList(itr); if (compo.size() != 3) { return QColor(); } return QColor((int)qClamp(compo[0], qreal(0), qreal(255)), (int)qClamp(compo[1], qreal(0), qreal(255)), (int)qClamp(compo[2], qreal(0), qreal(255))); } else { //QRgb color; //CSSParser::parseColor(name, color); return QColor(name); } } static QPainter::CompositionMode compositeOperatorFromString(const QString &compositeOperator) { if ( compositeOperator == "source-over" ) { return QPainter::CompositionMode_SourceOver; } else if ( compositeOperator == "source-out" ) { return QPainter::CompositionMode_SourceOut; } else if ( compositeOperator == "source-in" ) { return QPainter::CompositionMode_SourceIn; } else if ( compositeOperator == "source-atop" ) { return QPainter::CompositionMode_SourceAtop; } else if ( compositeOperator == "destination-atop" ) { return QPainter::CompositionMode_DestinationAtop; } else if ( compositeOperator == "destination-in" ) { return QPainter::CompositionMode_DestinationIn; } else if ( compositeOperator == "destination-out" ) { return QPainter::CompositionMode_DestinationOut; } else if ( compositeOperator == "destination-over" ) { return QPainter::CompositionMode_DestinationOver; } else if ( compositeOperator == "darker" ) { return QPainter::CompositionMode_SourceOver; } else if ( compositeOperator == "lighter" ) { return QPainter::CompositionMode_SourceOver; } else if ( compositeOperator == "copy" ) { return QPainter::CompositionMode_Source; } else if ( compositeOperator == "xor" ) { return QPainter::CompositionMode_Xor; } return QPainter::CompositionMode_SourceOver; } static QString compositeOperatorToString(QPainter::CompositionMode op) { switch (op) { case QPainter::CompositionMode_SourceOver: return "source-over"; case QPainter::CompositionMode_DestinationOver: return "destination-over"; case QPainter::CompositionMode_Clear: return "clear"; case QPainter::CompositionMode_Source: return "source"; case QPainter::CompositionMode_Destination: return "destination"; case QPainter::CompositionMode_SourceIn: return "source-in"; case QPainter::CompositionMode_DestinationIn: return "destination-in"; case QPainter::CompositionMode_SourceOut: return "source-out"; case QPainter::CompositionMode_DestinationOut: return "destination-out"; case QPainter::CompositionMode_SourceAtop: return "source-atop"; case QPainter::CompositionMode_DestinationAtop: return "destination-atop"; case QPainter::CompositionMode_Xor: return "xor"; case QPainter::CompositionMode_Plus: return "plus"; case QPainter::CompositionMode_Multiply: return "multiply"; case QPainter::CompositionMode_Screen: return "screen"; case QPainter::CompositionMode_Overlay: return "overlay"; case QPainter::CompositionMode_Darken: return "darken"; case QPainter::CompositionMode_Lighten: return "lighten"; case QPainter::CompositionMode_ColorDodge: return "color-dodge"; case QPainter::CompositionMode_ColorBurn: return "color-burn"; case QPainter::CompositionMode_HardLight: return "hard-light"; case QPainter::CompositionMode_SoftLight: return "soft-light"; case QPainter::CompositionMode_Difference: return "difference"; case QPainter::CompositionMode_Exclusion: return "exclusion"; default: break; } return QString(); } void Context2D::save() { m_stateStack.push(m_state); } void Context2D::restore() { if (!m_stateStack.isEmpty()) { m_state = m_stateStack.pop(); m_state.flags = AllIsFullOfDirt; } } void Context2D::scale(qreal x, qreal y) { m_state.matrix.scale(x, y); m_state.flags |= DirtyTransformationMatrix; } void Context2D::rotate(qreal angle) { m_state.matrix.rotate(qRadiansToDegrees(angle)); m_state.flags |= DirtyTransformationMatrix; } void Context2D::translate(qreal x, qreal y) { m_state.matrix.translate(x, y); m_state.flags |= DirtyTransformationMatrix; } void Context2D::transform(qreal m11, qreal m12, qreal m21, qreal m22, qreal dx, qreal dy) { QTransform mat(m11, m12, m21, m22, dx, dy); m_state.matrix *= mat; m_state.flags |= DirtyTransformationMatrix; } void Context2D::setTransform(qreal m11, qreal m12, qreal m21, qreal m22, qreal dx, qreal dy) { QTransform mat(m11, m12, m21, m22, dx, dy); m_state.matrix = mat; m_state.flags |= DirtyTransformationMatrix; } QString Context2D::globalCompositeOperation() const { return compositeOperatorToString(m_state.globalCompositeOperation); } void Context2D::setGlobalCompositeOperation(const QString &op) { QPainter::CompositionMode mode = compositeOperatorFromString(op); m_state.globalCompositeOperation = mode; m_state.flags |= DirtyGlobalCompositeOperation; } QVariant Context2D::strokeStyle() const { return m_state.strokeStyle; } void Context2D::setStrokeStyle(const QVariant &style) { if (style.canConvert()) { CanvasGradient cg = qvariant_cast(style); m_state.strokeStyle = cg.value; } else { QColor color = colorFromString(style.toString()); m_state.strokeStyle = color; } m_state.flags |= DirtyStrokeStyle; } QVariant Context2D::fillStyle() const { return m_state.fillStyle; } //! [3] void Context2D::setFillStyle(const QVariant &style) { if (style.canConvert()) { CanvasGradient cg = qvariant_cast(style); m_state.fillStyle = cg.value; } else { QColor color = colorFromString(style.toString()); m_state.fillStyle = color; } m_state.flags |= DirtyFillStyle; } //! [3] qreal Context2D::globalAlpha() const { return m_state.globalAlpha; } void Context2D::setGlobalAlpha(qreal alpha) { m_state.globalAlpha = alpha; m_state.flags |= DirtyGlobalAlpha; } CanvasGradient Context2D::createLinearGradient(qreal x0, qreal y0, qreal x1, qreal y1) { QLinearGradient g(x0, y0, x1, y1); return CanvasGradient(g); } CanvasGradient Context2D::createRadialGradient(qreal x0, qreal y0, qreal r0, qreal x1, qreal y1, qreal r1) { QRadialGradient g(QPointF(x1, y1), r0+r1, QPointF(x0, y0)); return CanvasGradient(g); } qreal Context2D::lineWidth() const { return m_state.lineWidth; } void Context2D::setLineWidth(qreal w) { m_state.lineWidth = w; m_state.flags |= DirtyLineWidth; } //! [0] QString Context2D::lineCap() const { switch (m_state.lineCap) { case Qt::FlatCap: return "butt"; case Qt::SquareCap: return "square"; case Qt::RoundCap: return "round"; default: ; } return QString(); } void Context2D::setLineCap(const QString &capString) { Qt::PenCapStyle style; if (capString == "round") style = Qt::RoundCap; else if (capString == "square") style = Qt::SquareCap; else //if (capString == "butt") style = Qt::FlatCap; m_state.lineCap = style; m_state.flags |= DirtyLineCap; } //! [0] QString Context2D::lineJoin() const { switch (m_state.lineJoin) { case Qt::RoundJoin: return "round"; case Qt::BevelJoin: return "bevel"; case Qt::MiterJoin: return "miter"; default: ; } return QString(); } void Context2D::setLineJoin(const QString &joinString) { Qt::PenJoinStyle style; if (joinString == "round") style = Qt::RoundJoin; else if (joinString == "bevel") style = Qt::BevelJoin; else //if (joinString == "miter") style = Qt::MiterJoin; m_state.lineJoin = style; m_state.flags |= DirtyLineJoin; } qreal Context2D::miterLimit() const { return m_state.miterLimit; } void Context2D::setMiterLimit(qreal m) { m_state.miterLimit = m; m_state.flags |= DirtyMiterLimit; } void Context2D::setShadowOffsetX(qreal x) { m_state.shadowOffsetX = x; m_state.flags |= DirtyShadowOffsetX; } void Context2D::setShadowOffsetY(qreal y) { m_state.shadowOffsetY = y; m_state.flags |= DirtyShadowOffsetY; } void Context2D::setShadowBlur(qreal b) { m_state.shadowBlur = b; m_state.flags |= DirtyShadowBlur; } void Context2D::setShadowColor(const QString &str) { m_state.shadowColor = colorFromString(str); m_state.flags |= DirtyShadowColor; } qreal Context2D::shadowOffsetX() const { return m_state.shadowOffsetX; } qreal Context2D::shadowOffsetY() const { return m_state.shadowOffsetY; } qreal Context2D::shadowBlur() const { return m_state.shadowBlur; } QString Context2D::shadowColor() const { return m_state.shadowColor.name(); } void Context2D::clearRect(qreal x, qreal y, qreal w, qreal h) { beginPainting(); m_painter.save(); m_painter.setTransform(QTransform(m_state.matrix), false); m_painter.setCompositionMode(QPainter::CompositionMode_Source); m_painter.fillRect(QRectF(x, y, w, h), QColor(0, 0, 0, 0)); m_painter.restore(); scheduleChange(); } //! [1] void Context2D::fillRect(qreal x, qreal y, qreal w, qreal h) { beginPainting(); m_painter.save(); m_painter.setTransform(QTransform(m_state.matrix), false); m_painter.fillRect(QRectF(x, y, w, h), m_painter.brush()); m_painter.restore(); scheduleChange(); } //! [1] void Context2D::strokeRect(qreal x, qreal y, qreal w, qreal h) { QPainterPath path; path.addRect(x, y, w, h); beginPainting(); m_painter.save(); m_painter.setTransform(QTransform(m_state.matrix), false); m_painter.strokePath(path, m_painter.pen()); m_painter.restore(); scheduleChange(); } void Context2D::beginPath() { m_path = QPainterPath(); } void Context2D::closePath() { m_path.closeSubpath(); } void Context2D::moveTo(qreal x, qreal y) { QPointF pt = m_state.matrix.map(QPointF(x, y)); m_path.moveTo(pt); } void Context2D::lineTo(qreal x, qreal y) { QPointF pt = m_state.matrix.map(QPointF(x, y)); m_path.lineTo(pt); } void Context2D::quadraticCurveTo(qreal cpx, qreal cpy, qreal x, qreal y) { QPointF cp = m_state.matrix.map(QPointF(cpx, cpy)); QPointF xy = m_state.matrix.map(QPointF(x, y)); m_path.quadTo(cp, xy); } void Context2D::bezierCurveTo(qreal cp1x, qreal cp1y, qreal cp2x, qreal cp2y, qreal x, qreal y) { QPointF cp1 = m_state.matrix.map(QPointF(cp1x, cp1y)); QPointF cp2 = m_state.matrix.map(QPointF(cp2x, cp2y)); QPointF end = m_state.matrix.map(QPointF(x, y)); m_path.cubicTo(cp1, cp2, end); } void Context2D::arcTo(qreal x1, qreal y1, qreal x2, qreal y2, qreal radius) { //FIXME: this is surely busted QPointF st = m_state.matrix.map(QPointF(x1, y1)); QPointF end = m_state.matrix.map(QPointF(x2, y2)); m_path.arcTo(st.x(), st.y(), end.x()-st.x(), end.y()-st.y(), radius, 90); } void Context2D::rect(qreal x, qreal y, qreal w, qreal h) { QPainterPath path; path.addRect(x, y, w, h); path = m_state.matrix.map(path); m_path.addPath(path); } void Context2D::arc(qreal xc, qreal yc, qreal radius, qreal sar, qreal ear, bool anticlockwise) { //### HACK // In Qt we don't switch the coordinate system for degrees // and still use the 0,0 as bottom left for degrees so we need // to switch sar = -sar; ear = -ear; anticlockwise = !anticlockwise; //end hack float sa = qRadiansToDegrees(sar); float ea = qRadiansToDegrees(ear); double span = 0; double xs = xc - radius; double ys = yc - radius; double width = radius*2; double height = radius*2; if (!anticlockwise && (ea < sa)) { span += 360; } else if (anticlockwise && (sa < ea)) { span -= 360; } //### this is also due to switched coordinate system // we would end up with a 0 span instead of 360 if (!(qFuzzyCompare(span + (ea - sa) + 1, 1) && qFuzzyCompare(qAbs(span), 360))) { span += ea - sa; } QPainterPath path; path.moveTo(QPointF(xc + radius * cos(sar), yc - radius * sin(sar))); path.arcTo(xs, ys, width, height, sa, span); path = m_state.matrix.map(path); m_path.addPath(path); } void Context2D::fill() { beginPainting(); m_painter.fillPath(m_path, m_painter.brush()); scheduleChange(); } void Context2D::stroke() { beginPainting(); m_painter.save(); m_painter.setTransform(QTransform(m_state.matrix), false); QPainterPath tmp = m_state.matrix.inverted().map(m_path); m_painter.strokePath(tmp, m_painter.pen()); m_painter.restore(); scheduleChange(); } void Context2D::clip() { m_state.clipPath = m_path; m_state.flags |= DirtyClippingRegion; } bool Context2D::isPointInPath(qreal x, qreal y) const { return m_path.contains(QPointF(x, y)); } ImageData Context2D::getImageData(qreal sx, qreal sy, qreal sw, qreal sh) { Q_UNUSED(sx); Q_UNUSED(sy); Q_UNUSED(sw); Q_UNUSED(sh); return ImageData(); } void Context2D::putImageData(ImageData image, qreal dx, qreal dy) { Q_UNUSED(image); Q_UNUSED(dx); Q_UNUSED(dy); } Context2D::Context2D(QObject *parent) : QObject(parent), m_changeTimerId(-1) { reset(); } const QImage &Context2D::endPainting() { if (m_painter.isActive()) m_painter.end(); return m_image; } void Context2D::beginPainting() { if (!m_painter.isActive()) { m_painter.begin(&m_image); m_painter.setRenderHint(QPainter::Antialiasing); if (!m_state.clipPath.isEmpty()) m_painter.setClipPath(m_state.clipPath); m_painter.setBrush(m_state.fillStyle); m_painter.setOpacity(m_state.globalAlpha); QPen pen; pen.setBrush(m_state.strokeStyle); if (pen.style() == Qt::NoPen) pen.setStyle(Qt::SolidLine); pen.setCapStyle(m_state.lineCap); pen.setJoinStyle(m_state.lineJoin); pen.setWidthF(m_state.lineWidth); pen.setMiterLimit(m_state.miterLimit); m_painter.setPen(pen); } else { if ((m_state.flags & DirtyClippingRegion) && !m_state.clipPath.isEmpty()) m_painter.setClipPath(m_state.clipPath); if (m_state.flags & DirtyFillStyle) m_painter.setBrush(m_state.fillStyle); if (m_state.flags & DirtyGlobalAlpha) m_painter.setOpacity(m_state.globalAlpha); if (m_state.flags & DirtyGlobalCompositeOperation) m_painter.setCompositionMode(m_state.globalCompositeOperation); if (m_state.flags & MDirtyPen) { QPen pen = m_painter.pen(); if (m_state.flags & DirtyStrokeStyle) pen.setBrush(m_state.strokeStyle); if (m_state.flags & DirtyLineWidth) pen.setWidthF(m_state.lineWidth); if (m_state.flags & DirtyLineCap) pen.setCapStyle(m_state.lineCap); if (m_state.flags & DirtyLineJoin) pen.setJoinStyle(m_state.lineJoin); if (m_state.flags & DirtyMiterLimit) pen.setMiterLimit(m_state.miterLimit); m_painter.setPen(pen); } m_state.flags = 0; } } void Context2D::clear() { endPainting(); m_image.fill(qRgba(0,0,0,0)); scheduleChange(); } void Context2D::reset() { m_stateStack.clear(); m_state.matrix = QTransform(); m_state.clipPath = QPainterPath(); m_state.globalAlpha = 1.0; m_state.globalCompositeOperation = QPainter::CompositionMode_SourceOver; m_state.strokeStyle = Qt::black; m_state.fillStyle = Qt::black; m_state.lineWidth = 1; m_state.lineCap = Qt::FlatCap; m_state.lineJoin = Qt::MiterJoin; m_state.miterLimit = 10; m_state.shadowOffsetX = 0; m_state.shadowOffsetY = 0; m_state.shadowBlur = 0; m_state.shadowColor = qRgba(0, 0, 0, 0); m_state.flags = AllIsFullOfDirt; clear(); } void Context2D::setSize(int width, int height) { endPainting(); QImage newi(width, height, QImage::Format_ARGB32_Premultiplied); newi.fill(qRgba(0,0,0,0)); QPainter p(&newi); p.drawImage(0, 0, m_image); p.end(); m_image = newi; scheduleChange(); } void Context2D::setSize(const QSize &size) { setSize(size.width(), size.height()); } QSize Context2D::size() const { return m_image.size(); } void Context2D::drawImage(DomImage *image, qreal dx, qreal dy) { if (!image) return; if (dx < 0) { qreal sx = qAbs(dx); qreal sy = qAbs(dy); qreal sw = image->width() - sx; qreal sh = image->height() - sy; drawImage(image, sx, sy, sw, sh, 0, 0, sw, sh); } else { beginPainting(); m_painter.drawImage(QPointF(dx, dy), image->image()); scheduleChange(); } } void Context2D::drawImage(DomImage *image, qreal dx, qreal dy, qreal dw, qreal dh) { if (!image) return; beginPainting(); m_painter.drawImage(QRectF(dx, dy, dw, dh).toRect(), image->image()); scheduleChange(); } void Context2D::drawImage(DomImage *image, qreal sx, qreal sy, qreal sw, qreal sh, qreal dx, qreal dy, qreal dw, qreal dh) { if (!image) return; beginPainting(); m_painter.drawImage(QRectF(dx, dy, dw, dh), image->image(), QRectF(sx, sy, sw, sh)); scheduleChange(); } //! [2] void Context2D::scheduleChange() { if (m_changeTimerId == -1) m_changeTimerId = startTimer(0); } void Context2D::timerEvent(QTimerEvent *e) { if (e->timerId() == m_changeTimerId) { killTimer(m_changeTimerId); m_changeTimerId = -1; emit changed(endPainting()); } else { QObject::timerEvent(e); } } //! [2]