If it helps simplify your project, I wrote a small library that outputs to NeoPixels using the SPI hardware. The code to send one byte is simply this on the Atmega328P:
void sendByte (byte b)
{
// send one byte to the Neopixels - note that the "off" gap is partly handled by the loop overhead
// gaps measured empirically to be 1.7 µs to 2 µs, so we don't need to add any more of our own
for (byte bit = 0; bit < 8; bit++)
{
if (b & 0x80) // is high-order bit set?
SPI.transfer (0b11111100); // 1 bit - 750 ns on + 250 ns off (acceptable "on" range 550 ns to 850 ns)
else
SPI.transfer (0b11100000); // 0 bit - 375 ns on + 625 ns off (acceptable "on" range 200 ns to 500 ns)
b <<= 1; // shift next bit into high-order position
} // end of for each bit
} // end of sendByte
One pixel is 3 bytes (green / red / blue):
void sendPixel (const byte r, const byte g, const byte b)
{
sendByte (g); // NeoPixel wants colors in green-then-red-then-blue order
sendByte (r);
sendByte (b);
} // end of sendPixel
The timing issue arises because the pixels "latch" if around 9µs elapses with no data. Thus you either precompute the data to be sent, or decide what to send very quickly.
Under the circumstances, using I2C, you would need to precompute the data, these is no way you could receive I2C on the fly and send it out to the Neopixels with them latching.
Is there some commonly used protocol over I2C for controlling NeoPixels ?
Not that I am aware of. I suggest you either send the pixel data (which would be a lot, and have to be broken into smallish packets), or send a "pattern number" and have the receiver generate the data.
Be aware that the standard I2C library has a 32-byte buffer so you can't send hundreds of bytes in a single I2C transaction.