/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2015 - ROLI Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ /** This scene description is broadcast to all the clients, and contains a list of all the clients involved, as well as the set of shapes to be drawn. Each client will draw the part of the path that lies within its own area. It can find its area by looking at the list of clients contained in this structure. All the path coordinates are roughly in units of inches, and devices will convert this to pixels based on their screen size and DPI */ struct SharedCanvasDescription { SharedCanvasDescription() {} Colour backgroundColour = Colours::black; struct ColouredPath { Path path; FillType fill; }; Array paths; struct ClientArea { String name; Point centre; // in inches float scaleFactor; // extra scaling }; Array clients; //============================================================================== void reset() { paths.clearQuick(); clients.clearQuick(); } void swapWith (SharedCanvasDescription& other) { std::swap (backgroundColour, other.backgroundColour); paths.swapWith (other.paths); clients.swapWith (other.clients); } // This is a fixed size that represents the overall canvas limits that // content should lie within Rectangle getLimits() const { float inchesX = 60.0f; float inchesY = 30.0f; return { inchesX * -0.5f, inchesY * -0.5f, inchesX, inchesY }; } //============================================================================== void draw (Graphics& g, Rectangle targetArea, Rectangle clientArea) const { draw (g, clientArea, AffineTransform::fromTargetPoints (clientArea.getX(), clientArea.getY(), targetArea.getX(), targetArea.getY(), clientArea.getRight(), clientArea.getY(), targetArea.getRight(), targetArea.getY(), clientArea.getRight(), clientArea.getBottom(), targetArea.getRight(), targetArea.getBottom())); } void draw (Graphics& g, Rectangle clientArea, AffineTransform t) const { g.saveState(); g.addTransform (t); for (const auto& p : paths) { if (p.path.getBounds().intersects (clientArea)) { g.setFillType (p.fill); g.fillPath (p.path); } } g.restoreState(); } const ClientArea* findClient (const String& clientName) const { for (const auto& c : clients) if (c.name == clientName) return &c; return nullptr; } //============================================================================== // Serialisation... void save (OutputStream& out) const { out.writeInt (magic); out.writeInt ((int) backgroundColour.getARGB()); out.writeInt (clients.size()); for (const auto& c : clients) { out.writeString (c.name); writePoint (out, c.centre); out.writeFloat (c.scaleFactor); } out.writeInt (paths.size()); for (const auto& p : paths) { writeFill (out, p.fill); p.path.writePathToStream (out); } } void load (InputStream& in) { if (in.readInt() != magic) return; backgroundColour = Colour ((uint32) in.readInt()); { const int numClients = in.readInt(); clients.clearQuick(); for (int i = 0; i < numClients; ++i) { ClientArea c; c.name = in.readString(); c.centre = readPoint (in); c.scaleFactor = in.readFloat(); clients.add (c); } } { const int numPaths = in.readInt(); paths.clearQuick(); for (int i = 0; i < numPaths; ++i) { ColouredPath p; p.fill = readFill (in); p.path.loadPathFromStream (in); paths.add (std::move (p)); } } } MemoryBlock toMemoryBlock() const { MemoryOutputStream o; save (o); return o.getMemoryBlock(); } private: //============================================================================== static void writePoint (OutputStream& out, Point p) { out.writeFloat (p.x); out.writeFloat (p.y); } static void writeRect (OutputStream& out, Rectangle r) { writePoint (out, r.getPosition()); out.writeFloat (r.getWidth()); out.writeFloat (r.getHeight()); } static Point readPoint (InputStream& in) { Point p; p.x = in.readFloat(); p.y = in.readFloat(); return p; } static Rectangle readRect (InputStream& in) { Rectangle r; r.setPosition (readPoint (in)); r.setWidth (in.readFloat()); r.setHeight (in.readFloat()); return r; } static void writeFill (OutputStream& out, const FillType& f) { if (f.isColour()) { out.writeByte (0); out.writeInt ((int) f.colour.getARGB()); } else if (f.isGradient()) { const ColourGradient& cg = *f.gradient; jassert (cg.getNumColours() >= 2); out.writeByte (cg.isRadial ? 2 : 1); writePoint (out, cg.point1); writePoint (out, cg.point2); out.writeCompressedInt (cg.getNumColours()); for (int i = 0; i < cg.getNumColours(); ++i) { out.writeDouble (cg.getColourPosition (i)); out.writeInt ((int) cg.getColour(i).getARGB()); } } else { jassertfalse; } } static FillType readFill (InputStream& in) { int type = in.readByte(); if (type == 0) return FillType (Colour ((uint32) in.readInt())); if (type > 2) { jassertfalse; return FillType(); } ColourGradient cg; cg.point1 = readPoint (in); cg.point2 = readPoint (in); cg.clearColours(); int numColours = in.readCompressedInt(); for (int i = 0; i < numColours; ++i) { const double pos = in.readDouble(); cg.addColour (pos, Colour ((uint32) in.readInt())); } jassert (cg.getNumColours() >= 2); return FillType (cg); } const int magic = 0x2381239a; JUCE_DECLARE_NON_COPYABLE (SharedCanvasDescription) }; //============================================================================== class CanvasGeneratingContext : public LowLevelGraphicsContext { public: CanvasGeneratingContext (SharedCanvasDescription& c) : canvas (c) { stateStack.add (new SavedState()); } //============================================================================== bool isVectorDevice() const override { return true; } float getPhysicalPixelScaleFactor() override { return 1.0f; } void setOrigin (Point o) override { addTransform (AffineTransform::translation ((float) o.x, (float) o.y)); } void addTransform (const AffineTransform& t) override { getState().transform = t.followedBy (getState().transform); } bool clipToRectangle (const Rectangle&) override { return true; } bool clipToRectangleList (const RectangleList&) override { return true; } void excludeClipRectangle (const Rectangle&) override {} void clipToPath (const Path&, const AffineTransform&) override {} void clipToImageAlpha (const Image&, const AffineTransform&) override {} void saveState() override { stateStack.add (new SavedState (getState())); } void restoreState() override { jassert (stateStack.size() > 0); if (stateStack.size() > 0) stateStack.removeLast(); } void beginTransparencyLayer (float alpha) override { saveState(); getState().transparencyLayer = new SharedCanvasHolder(); getState().transparencyOpacity = alpha; } void endTransparencyLayer() override { const ReferenceCountedObjectPtr finishedTransparencyLayer (getState().transparencyLayer); float alpha = getState().transparencyOpacity; restoreState(); if (SharedCanvasHolder* c = finishedTransparencyLayer) { for (auto& path : c->canvas.paths) { path.fill.setOpacity (path.fill.getOpacity() * alpha); getTargetCanvas().paths.add (path); } } } Rectangle getClipBounds() const override { return canvas.getLimits().getSmallestIntegerContainer() .transformedBy (getState().transform.inverted()); } bool clipRegionIntersects (const Rectangle&) override { return true; } bool isClipEmpty() const override { return false; } //============================================================================== void setFill (const FillType& fillType) override { getState().fillType = fillType; } void setOpacity (float op) override { getState().fillType.setOpacity (op); } void setInterpolationQuality (Graphics::ResamplingQuality) override {} //============================================================================== void fillRect (const Rectangle& r, bool) override { fillRect (r.toFloat()); } void fillRectList (const RectangleList& list) override { fillPath (list.toPath(), AffineTransform()); } void fillRect (const Rectangle& r) override { Path p; p.addRectangle (r.toFloat()); fillPath (p, AffineTransform()); } void fillPath (const Path& p, const AffineTransform& t) override { Path p2 (p); p2.applyTransform (t.followedBy (getState().transform)); getTargetCanvas().paths.add ({ std::move (p2), getState().fillType }); } void drawImage (const Image&, const AffineTransform&) override {} void drawLine (const Line& line) override { Path p; p.addLineSegment (line, 1.0f); fillPath (p, AffineTransform()); } //============================================================================== const Font& getFont() override { return getState().font; } void setFont (const Font& newFont) override { getState().font = newFont; } void drawGlyph (int glyphNumber, const AffineTransform& transform) override { Path p; Font& font = getState().font; font.getTypeface()->getOutlineForGlyph (glyphNumber, p); fillPath (p, AffineTransform::scale (font.getHeight() * font.getHorizontalScale(), font.getHeight()).followedBy (transform)); } private: //============================================================================== struct SharedCanvasHolder : public ReferenceCountedObject { SharedCanvasDescription canvas; }; struct SavedState { FillType fillType; AffineTransform transform; Font font; ReferenceCountedObjectPtr transparencyLayer; float transparencyOpacity = 1.0f; }; SharedCanvasDescription& getTargetCanvas() const { if (SharedCanvasHolder* c = getState().transparencyLayer) return c->canvas; return canvas; } SavedState& getState() const noexcept { jassert (stateStack.size() > 0); return *stateStack.getLast(); } SharedCanvasDescription& canvas; OwnedArray stateStack; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CanvasGeneratingContext) }; //============================================================================== /** Helper for breaking and reassembling a memory block into smaller checksummed blocks that will fit inside UDP packets */ struct BlockPacketiser { void createBlocksFromData (const MemoryBlock& data, size_t maxBlockSize) { jassert (blocks.size() == 0); int offset = 0; size_t remaining = data.getSize(); while (remaining > 0) { const int num = jmin (maxBlockSize, remaining); blocks.add (MemoryBlock (addBytesToPointer (data.getData(), offset), (size_t) num)); offset += num; remaining -= num; } MemoryOutputStream checksumBlock; checksumBlock << getLastPacketPrefix() << MD5 (data).toHexString() << (char) 0 << (char) 0; blocks.add (checksumBlock.getMemoryBlock()); for (int i = 0; i < blocks.size(); ++i) { uint32 index = ByteOrder::swapIfBigEndian (i); blocks.getReference(i).append (&index, sizeof (index)); } } // returns true if this is an end-of-sequence block bool appendIncomingBlock (MemoryBlock data) { if (data.getSize() > 4) blocks.addSorted (*this, data); return String (CharPointer_ASCII ((const char*) data.getData())).startsWith (getLastPacketPrefix()); } bool reassemble (MemoryBlock& result) { result.reset(); if (blocks.size() > 1) { for (int i = 0; i < blocks.size() - 1; ++i) result.append (blocks.getReference(i).getData(), blocks.getReference(i).getSize() - 4); String storedMD5 (String (CharPointer_ASCII ((const char*) blocks.getLast().getData())) .fromFirstOccurrenceOf (getLastPacketPrefix(), false, false)); blocks.clearQuick(); if (MD5 (result).toHexString().trim().equalsIgnoreCase (storedMD5.trim())) return true; } result.reset(); return false; } static int compareElements (const MemoryBlock& b1, const MemoryBlock& b2) { int i1 = ByteOrder::littleEndianInt (addBytesToPointer (b1.getData(), b1.getSize() - 4)); int i2 = ByteOrder::littleEndianInt (addBytesToPointer (b2.getData(), b2.getSize() - 4)); return i1 - i2; } static const char* getLastPacketPrefix() { return "**END_OF_PACKET_LIST** "; } Array blocks; }; //============================================================================== struct AnimatedContent { virtual ~AnimatedContent() {} virtual String getName() const = 0; virtual void reset() = 0; virtual void generateCanvas (Graphics&, SharedCanvasDescription& canvas, Rectangle activeArea) = 0; virtual void handleTouch (Point position) = 0; };