Drawing a one pixel line 2019

The browsers scale it all and it looks really good.

Yes that would be a perfect solution. I’ve hacked together a example here:

http://www.cssdesk.com/FzNWe

(Just a few rectangles with 1px width). Now no matter what I do (change windows to 125%, change the zoom in the browser), under no circumstances I end up with something like this:

which is the default outcome of the JUCE code (heavily zoomed in obviously):

for(int x = 0; x < 500; x += 100)
{
    g.drawVerticalLine(x, 0.0f, (float)getHeight());
}

if you have the Windows scale factor set to 125%.

In the browser example you get a line which is always at least 1px wide, but will get bigger if you zoom in. But it never, ever has these kind of alias blurs. If you zoom out that it would have to alias, it just moves the position of the line:

image

(You’ll notice that the height of the rectangles vary, but at least it doesn’t blur).

How come the entire internet seems to be completely fine with the idea of providing carefully-designed graphics that run on every imaginable size and shape of screen… but plugin GUI people are constantly banging on about this as if it’s some kind of essential feature?

Believe me, the last thing I want is to resort to some low-level graphics hacking. But you have to admit that the requirement to draw a single, completely unaliased line is not an out of the world request and this is really in the ballpark of the rendering framework to supply a method and not something that everyone has to write hacky helper tools for in their client code.

1 Like

Yes, unfortunately there’s no general way to get the current transform from the context, it’s at least platform dependant. I think you’d need to traverse the Component hierarchy applying each transform in turn.

That’s exactly my point. It’s not a drawing primitive, it’s a helper function . It doesn’t need access to Graphics internals and doesn’t mutate the state. Thereby it should be a non-member function.

Just tried to use different zoom steps in Safari on a non retina monitor, interesting is that with even non-integer zoom factors, the boundaries of div-box rely on physical pixels.
So the comparison between JUCE and browser engines is technically not correct.
Even if I agree that this time GUI-design should not be bounded to physical resolution, its also an aesthetic question, especially on non retina monitors.

BTW, if non-physical reliance is so important, this should be fixed

Alright, hacky client-code coming in…

/** A small helper class that will draw pixel-aligned shapes without blurrying the edges. */
class UnblurryGraphics
{
public:

	/** Creates an object for drawing unblurred stuff. 
	
		You need to supply the Graphics as well as the component you're about to draw onto.
		It will calculate the ratios on construction, so if you're about to use this multiple
		times within one paint() callback it saves a few CPU cycles. 

		It's intended to be used as drop-in replacement for the Graphics object:

		    void paint(Graphics& g) override
			{
			    UnblurryGraphics ug(g, *this);
				
				ug.draw1PxRect(getLocalBounds().toFloat().reduced(5.0f));
			}

		Be aware that it accepts float values as argument to each method as it tries to
		postpone the rounding as much as possible.
	*/
	UnblurryGraphics(Graphics& g_, Component& componentToDrawOn) :
		g(g_),
		c(componentToDrawOn),
		tl(c.getTopLevelComponent())
	{
		juceScaleFactor = UnblurryGraphics::getScaleFactorForComponent(&c);
		sf = g.getInternalContext().getPhysicalPixelScaleFactor();
		physicalScaleFactor = sf / juceScaleFactor;

		// Now for some reason a small rounding error is introduced, so we make
		// sure that the physical scale factor is a multiple of 0.25.
		// (I am not aware of OS that use a smaller resolution for their scale factor
		// steps).
		physicalScaleFactor -= fmodf(physicalScaleFactor, 0.25f);
		sf = juceScaleFactor * physicalScaleFactor;

		pixelSizeInFloat = 1.0f / sf;

		// On retina images, this will make sure that it draws something that resembles 1 px wide thingies.
		pixelSizeInFloat *= std::floor(sf);

		// For the position calculation we just need the physical scale factor
		subOffsetDivisor = 1.0f / physicalScaleFactor;
	}

	/** Draws a 1px horizontal line without blurrying the edges. */
	void draw1PxHorizontalLine(float y, float startX, float endX)
	{
		auto x = getRoundedXValue(startX);
		auto w = getRoundedXValue(endX) - x;

		g.fillRect(x,
			getRoundedYValue(y),
			w,
			pixelSizeInFloat);
	}

	/** Draws a 1px thick vertical line without blurrying the edges. */
	void draw1PxVerticalLine(float x, float startY, float endY)
	{
		auto y = getRoundedYValue(startY);
		auto h = getRoundedYValue(endY) - y;

		g.fillRect(getRoundedXValue(x),
			y,
			pixelSizeInFloat,
			h);
	}

	/** Draws a 1px wide rectangle without blurrying the lines. */
	void draw1PxRect(Rectangle<float> rect)
	{
		auto x = getRoundedXValue(rect.getX());
		auto y = getRoundedYValue(rect.getY());
		auto w = getRoundedXValue(rect.getRight()) - x;
		auto h = getRoundedYValue(rect.getBottom()) - y;

		g.drawRect(x, y, w, h, pixelSizeInFloat);
	}

	/* fills a float rectangle without blurrying the edges. */
	void fillUnblurryRect(Rectangle<float> rect)
	{
		auto x = getRoundedXValue(rect.getX());
		auto y = getRoundedYValue(rect.getY());
		auto w = getRoundedXValue(rect.getRight()) - x;
		auto h = getRoundedYValue(rect.getBottom()) - y;

		g.fillRect(x, y, w, h);
	}

private:

	static float getScaleFactorForComponent(Component* c)
	{
		float sf = c->getTransform().getScaleFactor();
		auto pc = c->getParentComponent();

		while (pc != nullptr)
		{
			sf *= pc->getTransform().getScaleFactor();
			pc = pc->getParentComponent();
		}

		return sf;
	}

	float getRoundedXValue(float xValue) const
	{
		// Get the point relative to the top-level component
		// this will factor in any scale factor we've set.
		auto xToUse = tl->getLocalPoint(&c, Point<float>(xValue, 0.0f)).getX();
		auto tmp = roundToInt(xToUse / subOffsetDivisor);
		auto xInTopLevel = (float)tmp * subOffsetDivisor;
		return c.getLocalPoint(tl, Point<float>(xInTopLevel, 0.0f)).getX();
	}

	float getRoundedYValue(float yValue) const
	{
		// Get the point relative to the top-level component
		// this will factor in any scale factor we've set.
		auto yToUse = tl->getLocalPoint(&c, Point<float>(0.0f, yValue)).getY();
		auto tmp = roundToInt(yToUse / subOffsetDivisor);
		auto yInTopLevel = (float)tmp * subOffsetDivisor;
		return c.getLocalPoint(tl, Point<float>(0.0f, yInTopLevel)).getY();
	}

	Graphics& g;
	Component& c;
	Component* tl;

	float juceScaleFactor;
	float sf;
	float physicalScaleFactor;
	float pixelSizeInFloat;
	float subOffsetDivisor;
};

Feel free to use it however you like - I also have no objections if this makes it into the JUCE codebase after the usual heavy refactoring :slight_smile:

I’ve only tested it on Windows, but it takes the Windows scale factor as well as any JUCE scale factor into account (weirdly, Windows scale factor 125% + AffineTransform::scale(1.25) yields a scale factor of 1.56374... instead of 1.5625, so I had to hack around to correct this to the expected value, so maybe someone with more knowledge about scale factors might chime in here).

It also draws 2px wide lines when the scale factor is >= 2.0 (and for the future with scale factors of 1000.0 it should also be creating a 1000px wide line). The solution was rather genius: I multiplied the pixel size with the scale factor :slight_smile:

3 Likes

Is there a better way to do these? I have boxes that need rounded rectangle borders for selection:

#pragma once
#include "../JuceLibraryCode/JuceHeader.h"


class Box : public Component
{
public:

private:
	void paint(Graphics& g) override
	{
		g.setColour(Colours::black);
		g.drawRoundedRectangle(getLocalBounds().toFloat(), 3.0f, 1.0f);
	}
};


class MainComponent : public Component
{
	std::vector<std::unique_ptr<Box>> boxes;
	OpenGLContext context;

public:
	MainComponent()
	{
		context.attachTo(*this);

		for (size_t i = 0; i < 1000; ++i)
		{
			boxes.emplace_back(std::make_unique<Box>());
			addAndMakeVisible(*boxes.back());
		}

		setSize(500, 500);
	}
	~MainComponent()
	{
		context.detach();
	}

private:
	void paint(Graphics& g) override
	{
		g.fillAll(Colours::grey);
	}

	void resized() override
	{
		size_t i = 0;
		for (int y = 0; y < getHeight(); y += 50)
			for (int x = 0; x < getWidth(); x += 50)
				if (i < boxes.size())
					boxes[i++]->setBounds(x, y, 25, 25);
	}
};

Unfortunately some of them don’t even have 4 rounded corners.

Round rectangles are using paths behind the scenes AFAIK so this won‘t work here.

Thanks for the unblurry graphics class btw gonna try that out in a bit for grid lines.

For some reason horizontal lines ares still a bit blurry (the higher the y value the higher the blur) so there must be an rounding error somewhere which is weird because the code is 100% equivalent for x and y axis.