GithubHelp home page GithubHelp logo

besnoi / iffy Goto Github PK

View Code? Open in Web Editor NEW
23.0 1.0 2.0 311 KB

A SpriteSheet and Tileset helper library for Love2D (handles Tilemaps as well)

Lua 100.00%
love2d-library spritesheet love2d tilesets individual-sprites

iffy's Introduction

Iffy

Iffy is basically a helper library which makes working with spritesheets and tilesets a *hell lot* easier. Load sprite data from XML or CSV files and render sprites just like you'd render images. If that's not all you can also add sprite data (like you'd with other libraries) and if you want you could export the sprite-data (that you created) to XML,CSV files so that you could load from that later (or some external program might load from that). Oh and yes - Iffy can also help you with tilemaps that were exported by Tiled. Not just Tiled but also with tilemaps in the form of Lua tables. Iffy could also help you with loading Tilesets. Have a weird margin problem in the tileset? Don't worry Iffy will fix that for you. Want to render a tilemap for a specific tileset? Iffy does that as well.

Note that Iffy helps only with static images - and by that I mean Iffy can't help you with animations and also it can't help you with AutoTiles. Now if you are disappointed by hearing that and feel like closing this tab, head over to A quick walkthrough for the motivation you need

Table of Contents

How to use Iffy?

Since it's a module you need to require it

	iffy = require 'iffy'

If iffy is not in the same folder as main.lua then you'd say something like require 'pathto/iffy'.

A Quick Walkthrough

Have you ever used Kenney's assets? Then you may know that all of Kenney's assets are "game-ready". Meaning you don't have to tweak anything or do some extra work to load those assets. Every spritesheet of Kenney - unless it's a tileset or has animations in it - has an XML file by the same name. Now let's not talk much. Let me give you context. Here's a spritesheet made by Kenney (on left is the image 'diceRed.png' and on right is the metafile 'diceRed.xml')

That's so nice of Kenney that he provided us with an XML file containing all the information about the individual sprites : but how do we use it - in LOVE2D. That's where Iffy comes to the rescue. With Iffy you'd do something like this:-

iffy.newAtlas("diceRed.png")

And that's it!! Now if you want to draw an individual sprite you'd do something like this

iffy.drawSprite('diceRed6.png')

Ofcourse you could provide additional parameters for x,y,r,sx,sy,etc. But we set all to default for making this example simple. Now note here that diceRed6.png comes from the XML file. To remove the '.png' - if you want to - you could easily replace all instances of '.png' with '' in the XML file.

Second to XML, there's a very popular format for storing sprite data - CSV!! What if your sprite data was not stored in XML but in CSV. Don't worry- Iffy does that as well. Let's say you have two files "dice.png" and "dice.csv" like this:-

So just like before - infact exactly the same as before (BTW Don't worry about the extra data. Iffy will ignore them) The way you would load and draw a sprite is :-

iffy.newAtlas("dice.png")

function love.draw()
	iffy.drawSprite('dice4')
end

Documentation

Ah!! It was just yesterday that I wrote this library in my spare time and here I'm - writing its documentation. Anyways let's get started already- let's first start by seeing how Iffy works and then take a quick glance at all of iffy's functions

How Iffy works?

Before we move on to overloads of the functions. Let me - very briefly, and on a high-level - state how iffy works. Iffy keeps a database of sprites each mapped to the atlas they were captured from. Both the sprites and images have a name so they could be identified. While the image names must be unique the sprite-names do not have to be unique unless they belong to the same atlas. While you could access a sprite without specifying which atlas it belongs to (using functions like getSprite, drawSprite, etc) - it's recommended that you be more precise and use functions like get and draw instead of their counterparts - if you have sprite-names by the same name mapped to different atlas. For more info in this matter head over here

Just as a side-note to speed up accessing sprites, Iffy also maintains cache.

Iffy also keeps a database of the sprite-data - if you are manually ripping out sprites from their atlases using Iffy's newSprite. This is so that this data could be exported later - if you want to - using exportCSV or exportXML.

Enough talk about spritesheets, let's move over to tilesets and tilemaps. Even though Iffy treats a tile just like a sprite, Iffy has a dedicated database for the tilesets that were created - mainly to get the tilewidth and tileheight so that it could render the tilemaps (for these tilesets) correctly.

And as a quick note here - In Iffy tiles are accessed by numbers whereas sprites are accessed by their names.

Now loading and rendering tilemaps are - unlike the dedicated libraries - very easy in Iffy. This however doesn't mean Iffy is a replacement for ATL or STL. Iffy doesn't have any feature for AutoTiles and Iffy can only load CSV format tilemaps (ones exported by Tiled) or load tilemaps from a lua table and then to render them you need to pass the tilemap name and the tileset name.

The above is not the same as saying that Iffy isn't good with tilesets. As long as you don't want auto-tiles and go fancy with it - Iffy can help you a lot with tilemaps. First of all it helps keeps data and code seperate in that the maps are in the CSV format and not in Lua format. You can very easily have layers using Iffy - just have more than one tilemaps and render them accordingly. (The down-side is that then that'd be "two or more tilemaps" meaning more than one file to render a single map)

And if I didn't say this earlier then: Iffy also maintains a database of the tilemaps that were loaded via a CSV file or a lua table. And I don't think I said this in the beginning but Iffy also maintains a database of all the images that were loaded either from file or passed as argument and the image names - if i didn't say this earlier - are mapped to each of these images so that when rendering Iffy'd know which image to render just from a string passed in as argument. Now it may look like Iffy wastes a lot of memory but it doesn't. Most of the data-base entries are references of one-another. Even Iffy's cache stores only references of the already created objects and not a seperate object of its own.

A quick glance at all the functions

Function Overloads Description
newSprite 3 Makes a brand new sprite from the given parameters and returns the quad that was created
newImage 3 Adds the given image to the Iffy's database
newAtlas 3 Makes a brand new spritesheet from the provided image and metafile and returns a reference to the htable of image-quads that were created
newTileset 3 Makes a brand new tileset from the given parameters
newTilemap 3 Loads a tilemap from a CSV file or a table in the same format
drawTilemap 1 Draws a given tilemap for a given tileset
getImage 1 Gets a reference of the image from Iffy's database
getSprite 1 Gets a reference of the sprite and the atlas it belongs to from Iffy's database
duplicateSprite 1 Make aliases for a sprite name
get 1 Gets a reference of the sprite from a particular atlas from Iffy's database
drawSprite 1 Draws a sprite without asking the end-user to specify- for which atlas?
draw 1 Draws a sprite from a given atlas
exportCSV 1 Exports the sprite-data to a CSV file
exportXML 1 Exports the sprite-data to a XML file

Creating a SpriteSheet using Metafile

In this section we will look at how one can create a spritesheet (create all the quads for further use) using an external metafile (some XML or CSV)

newAtlas

So newAtlas - the function which we looked at in our quick walkthrough. But we didn't discuss the overloads there and maybe you didn't completely understand what it does - within the hood - for you, even after reading the description.

So what newAtlas does is - it loads all the data from the meta-file provided for the image and creates the quads based on that. Since each quad needs to identified later on it also needs an image name to namespace a sprite-name within that name. (BTW the sprite-name is present in the metafile) If you want you can also scale the object by providing extra sw,sh parameters.

Now there are 4 overloads to newAtlas. Let's look at the most descriptive one first :-

<table> newAtlas(<string> name, <string> img_url, <string> metafile, [<number> sw,<number> sh])     --Overload #1
<table> newAtlas(<string> name, <userdata>   img, <string> metafile, [<number> sw,<number> sh])     --Overload #2
  1. So the first parameter is the name which is the name of the image. I want to stress this over and over again that Iffy has its own database. It doesn't depend on you to pass the image during rendering a sprite. Rather it keeps a reference to the image which it accesses by an key and this key is decided by you!

  2. So anyways the second parameter is either the url of the image (overload #1) (in case you haven't loaded the image on your own) or the reference to the image (overload #2).

  3. The third parameter is the url of the metafile - the XML or the CSV file which has the data concerning the sprite's name, dimensions and position in the atlas. To understand the format in which data should go I suggest you look at the reference images- for xml and for csv. We anyways'll talk about the format in this section.

  4. And the fourth and fifth parameter - like I said in the beginning - are the image dimensions which are by default the image dimensions.


But it was so simple in the walkthrough? - You might say. In a moment we'll find out it still is!

Here comes Overload #3 :

<table> newAtlas(<string> img_url, [<string> metafile])     --Overload #3

So in the third overload we only pass the url of the image and the url of the metafile. As you may know sw,sh are defaulted to the image's dimensions and the image name - name is defaulted to the filename of the image. For eg. for url 'assets\hello.png' the image name will be 'hello'.

It is interesting to note that metafile is within square brackets. So you can only pass the image url and it'd still work. It would find the name automatically like it did earlier and it'll be assumed that the metafile is in the same location as the image and has the same filename except for the extension (no extension,".xml",".csv" or ".txt" supported).


Now I'd like to point out here that even though newAtlas returns a table yet it's not mandatory for you to store it. Especially not if you are going to use Iffy's functions to render the sprites.

Creating a SpriteSheet without any metafile

In this section will look at how you can create a spritesheet like you - most likely - had been before you bumped into Iffy (assuming you'd use Iffy from now on). So you create each sprite individually. But even then the iffiness would still be there. How? Well you'll find out on your own

newImage

When creating a spritesheet using newAtlas or newTileset the image is loaded onto Iffy's database automatically. But if you want to load an image manually - maybe to use newSprite - you can always use newImage

<nil> newImage(<string> img_name, <string> img_url)                                              --Overload #1
<nil> newImage(<string> img_name, <userdata> img)                                                --Overload #2
<nil> newImage(<string> img_url)                                                                 --Overload #3

So you pass in the name of the image and the url or the reference of the image and Iffy would do its thing. If the name of the image is the same as the image's filename then you only pass the url of the image

newSprite

Sometimes you may not have a metafile and actually that counts as "most of the time"! And most of the libraries are aligned towards that situation. So how could Iffy leave behind!

<userdata> newSprite(<string> img_name, spr_name, <number> x, y, width, height, <userdata> img)  --Overload #1
<userdata> newSprite(<string> img_name, spr_name, <number> x, y, width, height, sw, sh)          --Overload #2
<userdata> newSprite(<string> img_name, spr_name, <number> x, y, width, height)                  --Overload #3

I think some of it is trivial - you pass in the name of the image and the sprite-data i.e. the name,position and dimensions of the sprite and then finally the dimensions of the image. You can either pass the image itself (Overload #1) and Iffy would do its thing or you could pass in the dimensions of the image manually (Overload #2). If however the image was loaded before using newImage then you don't need to provide either of those (Overload #3) It'd return a Quad.

Note that newSprite doesn't load an image (even if you used Overload #1). You must load the image (by the name you used in newSprite) And by loading an image I mean loading an image onto Iffy's database which you would normally do using newImage. It doesn't matter in which order you load the image and create the sprite - the whole thing is about loading the image before rendering the sprite.

Exporting your SpriteData

Please don't export the sprite-data if your game is packaged as love! This may cause issues.

Now that you have created the sprites manually you may want to export them to some generic format. Maybe to want to use them later with newAtlas or whatever may be the reason. Iffy supports two formats - XML and CSV both of which are wildly used in the sprite world.

Note here that all of this is relevant only for spritesheets. I can't imagine the situation where you would want to export tileset data (that'd be kinda lame though). Also note that if you try to export a file which is already present then Iffy will warn you that that file already exists (that doesn't mean it wont' overwrite it - it will just let you know)

exportXML

<nil> exportXML(<string> img_name, [<string> path, <string> filename])

The img_name is the name of the image, path is the path where you want to store the metafile and filename is the name of the metafile.

Note that the path is defaulted to "" and the filename is defaulted to the image name plus ".xml"

exportCSV

<nil> exportCSV(<string> img_name, [<string> path, <string> filename])

Everything is exactly the same as exportXML just the filename is defaulted to the image name plus ".csv" (instead of ".xml")

Drawing a Sprite (or a Tile)

Everything's great so far. But how do you draw stuff? Since the image quads are returned in every init functions you could use love.draw but really it won't look good. So Iffy has it's own draw functions - one generic (draw) and one specific (drawSprite) So let's take a quick look at both of them

draw

<nil> draw(<string> img_name,<string> sprite_name, ...)

I won't type in too many lines for this function. You pass in the image name and the sprite name (or the tile no if you are using tileset) and then the regular parameters for position,rotation,etc in the ...

drawSprite

<nil> drawSprite(<string> sprite_name, ...)

Unlike draw, drawSprite doesn't need an image name - it'll automatically find the image name which has a sprite by the given name. And once found it'd store it to cache for further use and easy retrieval. The ... holds the regular paramters like position,rotation,etc. BTW even though you could technically but it's not recommended that you draw a tile using drawSprite. To be precise drawSprite as the name suggests was always meant only for drawing sprites. So it's recommended you don't draw a tile with it. Use draw instead (if you have to draw a single tile)

Miscellaneous Operations on Sprites

get

<userdata> get(<string> img_name,<string> sprite_name)

If you don't plan to use draw then you may want to use get which simply returnss the Quad.

getSprite

<userdata,userdata> getSprite(<string> sprite_name)

Unlike get, getSprite doesn't need an image name - it'll automatically find the image name which has a sprite by the given name. However getSprite returns two userdata - Drawable (the alleged image that was found) and Quad (the actual sprite)

Just as a side note here - getSprite is used internally by drawSprite

duplicateSprite

<nil> duplicateSprite(<string> img_name, <string> orig_sprite_name, <string> alias_sprite_name)

What if you want more than one spritename perhaps for your own ease! duplicateSprite will basically make an alias of the sprite so that you could access the same Quad with more than one name.

Iffy and the world of Tiles

newTileset

Second to newAtlas there's another function which helps you in making a spritesheet very quickly - which is newTileset. Now a tileset is not the same as a spritesheet. Iffy treats both of them differently - a sprite is keyed by a unique name (in the atlas) but a tile is is indexed by a number starting from top to bottom and left to right from 1.

There are 3 main overloads which I'm just gonna wave my hand over:-

<table> newTileset(<string> tset_name, <string> img_url, <number> [tw, th, mx, my, sw, sh] )     --Overload #1
<table> newTileset(<string> tset_name, <userdata>   img, <number> [tw, th, mx, my, sw, sh] )     --Overload #2
<table> newTileset(<string> img_url)                                                             --Overload #3

Now just like spritesheet, tilesets too have names to identify them later. And of-course the tileset image need to be passed either as url or as reference. (#1 and #2). But what are these other parameters?

tw,th are the tile-widths and tile-heights, the are 32,32 by default. mx,my are the margin/spacing* between the tiles. (Note that the first tile must be at (0,0) in the tileset) and the last parameters sw,sh are the atlas dimensions which are by default the same as the image's dimensions

The terms margin and spacing are not interchangeable but Kenney uses them interchangeably so I'm just following on his footsteps

newTilemap

<table> newTilemap(<string> tmap_name, <string> map_url)                                         --Overload #1
<nil>   newTilemap(<string> tmap_name, <table>  map)                                             --Overload #2
<table> newTilemap(<string> map_url)                                                             --Overload #3

Just like tilesets and spritesheets, tilemaps too have names. So you pass that and the map_url which is the path to the CSV file exported by Tiled (btw you can even handcraft the file). And then a table is returned which is the tilemap that was the loaded from the file.

But in case you already have a table then you may very well pass that so that Iffy would store a reference to it in its database. And note nothing is returned since it wouldn't make sense to return the same table that you passed in as argument.

The third overload however requires that you pass in the url of the map and then the tilemap name is calculated based on that (basically the filename minus the extension)

drawTilemap

<nil> drawTilemap(<string> map_name, <string> tileset_name, <number> [offset, mx, my])

To draw a tileset ofcourse you need to pass in the map name and then the tileset name specifying which tileset you want to use (cause this information is not in the tilemap). The third parameter offset basically adds itself to every tile number (received from the tilemap). For context - Tiled starts indexing from 0 but the tilesets in Iffy are numbered from 1 (since we are programming in Lua) so that'd cause an off-by-one error. To prevent such errors you add an offset of 1 to each tile number. By default offset is set to 0

Note that in Iffy, zero and negative numbers are void tiles meaning no tile exist at that position. However for Tiled, -1 is the void tile.

The last parameters mx,my are the margin (not the spacing, as it was the case in newTileset that we used the words interchangeably). Basically it says from where to draw the tileset. By default it is 0,0

Metafile format used by Iffy

Spoiler: If you simply want to use Kenney's assets then skip this section. The only relevant section for you is A quick walkthrough

For XML Iffy expects the metadata to be in the given format (note that there mustn't be any other tag (even prologue tag) though comments and blanklines are supported)

<!-->Ofcourse the tagnames could be different</!-->
<TextureAtlas imgPath="sheet.png">
	<SubTexture name="sprite_01" x="0" y="0" width="32" height="32"/>
</TextureAtlas>

This is the format that Kenny stores his atlases in. He uses some software to merge his images into a spritesheet. Please note that while it's not mandatory for the tagnames to be exact but it's mandatory that the tree-like structure is maintained! Also there are some trivial rules that your meta-file must follow:-

  • The x,y,width and height must be attributes (in the same line) and not tags
  • The attributes name must be the same
  • The quotes around the attribute values are must

And the second format- one for CSV files is :-

"sprite_name", x, y , width, height

Hopefully Iffy is not so strict with the CSV format. You can have blank lines, no quotes surrounding the first attribute, as many surrounding white spaces as you want - Iffy will trim them for you. You can even have comments (one beginning with '#'). And as you saw in the quick walkthrough you can even have some extra data which Iffy will simply ignore. But there are some trivial rules which your meta-file must comply with, these general rules are :-

  • The sprite name may or may not be in quotes (Iffy assumes that the sprite name won't have any quotes so any quotes will simply be ignored)
  • Other attributes must not be in quotes no matter what
  • All the data must be in one line

Aliases used by Iffy

Here are the aliases used by Iffy:-

	iffy.newSpritesheet = iffy.newAtlas
	iffy.newSpriteSheet = iffy.newAtlas
	iffy.newTileMap     = iffy.newTilemap
	iffy.newTileSet     = iffy.newTileset

Feedback?

Do you see yourself using this library? If no, why? What could be improved? You can drop all your constructive comments,etc under Issues and I'll look at them or you could even mail me. Oh and since this is GitHub you are free to fork it (make your changes,etc. Iffy along with all the other libraries under lovelib is licensed under GPLv3 which is more flexible than the MIT license. So you should take advantage of it and maybe sell the library to some n00b - just kidding :laugh:) you can make a PR if you feel like it and I can't guarantee but most of the time I'll accept the changes you made.

iffy's People

Contributors

andsleonardo avatar besnoi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

iffy's Issues

newTileset() tw/th reversed

on lines 252 and 253 of iffy.lua, tw and th are reversed (I only discovered this because my tileset is not square)

				(j-1)*th + (current==1 and 0 or mx),
				(i-1)*tw + (current==1 and 0 or my),

I/O lib doesn't work on some OS (with Love2D, pure Lua has no problems)

Hey,
Just letting you know io.open doesn't work for both me and my coprogrammer. We had to replace those functions with love.filesystem.read (or love.filesystem.lines for infile:lines() interations).
Just FYI, we got it handled but you might post a warning or something for other users (it took us a bit of time to figure out)

accepting PRs?

hey, outstanding work! i found it pretty easy to add animations and sumneko lua annotations, wonder if you'd accept PRs to integrate them.

Example annotations

--- Maps given image name with the provided image.
--- @param iname string The name of the image
--- @param url string? The url or reference to the image
--- @overload fun(iname: string, url?: love.Image)
function iffy.newImage(iname, url)
  if not url then
    url = iname
    iname = removeExtension(url)
  end
  iffy.images[iname] = type(url) == 'string' and love.graphics.newImage(url) or url --[[@as love.Image]]
end
--- Makes a brand new sprite (image-quad) from given parameters.
---
--- Before calling this function make sure you map iname with the image
--- using newImage otherwise you won't be able to render the sprite with iffy
--- @param iname string The name of the Image (needed to namespace the sprite)
--- @param name string The name of the Sprite (needed to locate the sprite)
--- @param x number The position of the sprite in the atlas
--- @param y number The width of the sprite
--- @param width number The width of the sprite
--- @param height number Not needed if a reference (not url) to the image is provided
--- @param sw ?number
--- @param sh ?number
--- @return love.Quad
function iffy.newSprite(iname, name, x, y, width, height, sw, sh)
  local image = iffy.images[iname]

  if not sw and not image then
    error("Iffy Error! " ..
      "You must provide the size of the image in the last parameter " ..
      "in the function 'newSprite'"
    )
  end
  if not sw then
    sw = image:getWidth()
  end
  if not sh then
    sh = image:getHeight()
  end
  if not iffy.spritesheets[iname] then iffy.spritesheets[iname] = {} end
  if not iffy.spritedata[iname] then iffy.spritedata[iname] = {} end

  iffy.spritesheets[iname][name] = love.graphics.newQuad(x, y, width, height, sw, sh)
  table.insert(iffy.spritedata[iname], { name, x, y, width, height })
  return iffy.spritesheets[iname][name]
end

IDE screenshots with annotations

image
image

Basic Animation module

local Atlas = require "src.tool.atlas"
local Animation = {}

---@param name string an identifier for the animation
---@param atlas string the name of the atlas to use, loaded from iffy
---@param frames table<number, string> a table of frame numbers and sprite names, mapped to the atlas defined keys
---@param fps number the number of frames per second to play
---@param loop boolean whether or not to loop the animation
function Animation.new(name, atlas, frames, fps, loop)
  local self = {}

  self.name = name
  self.atlas = atlas
  self.frames = frames
  self.fps = fps
  self.loop = loop

  self.currentFrame = 1
  self.currentTime = 0

  function self.update(dt)
    if dt > 0.1 then -- clamp to prevent the animation from updating too quickly and freezing the game
      dt = 0.1
    end
    self.currentTime = self.currentTime + dt
    if self.currentTime >= 1 / self.fps then
      self.currentTime = self.currentTime - 1 / self.fps
      self.currentFrame = self.currentFrame + 1
      if self.currentFrame > #self.frames then
        if self.loop then
          self.currentFrame = 1
        else
          self.currentFrame = #self.frames
        end
      elseif self.currentFrame < 1 then
        if self.loop then
          self.currentFrame = #self.frames
        else
          self.currentFrame = 1
        end
      end
    end
  end

  function self.draw(x, y, r, sx, sy)
    Atlas.lib.drawSprite(self.frames[self.currentFrame], x, y, r, sx, sy) -- Atlas.lib = iffy
  end

  return self
end

return Animation

Usage

local Atlas = require "src.tool.atlas"
local Animation = require "src.tool.animation"

local random_npc_reward = {}

function love.load()
  love.graphics.setDefaultFilter("nearest", "nearest")

  Atlas.Export() -- generates assets/main_atlas.xml
  Atlas.Load()   -- loads assets/main_atlas.xml into memory, `main_atlas` is now available

  random_npc_reward = Animation.new("random_npc_reward", "main_atlas", {
    [1] = "npc_merchant",
    [2] = "npc_knight",
    [3] = "npc_kid",
  }, 6, true)
end

function love.draw()
  random_npc_reward.draw(200, 200, 0, 4, 4)
end

function love.update(dt)
  random_npc_reward.update(dt)
end

Let me know your thoughts. I'm picking iffy because of how scalable I think this library is, others are way too opinionated and not flexible enough.

Also noticed the license is missing

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.