Git Product home page Git Product logo

fray's Introduction

Hey there 👋

Hey! My name is Ezekiel, and I am a recent computer science graduate with an interest in game development and software design. I love using my programming skills to bring new ideas to life, and I'm always looking for ways to learn and grow in my field. When I'm not coding, I enjoy exploring my creative side through digital art and I occasionally share my latest creations on my Instagram. Thanks for taking the time to get to know me!


😎 About Me

  • 🔭 I’m currently working on Fray, a fighting/action game framework for the Godot game engine! As a fan of fighting games, I noticed that there is a lack of resources and tools available to aid in the development of such games. So I started building an addon that helps with some common fighting game issues, such as state management, hitbox management, and complex input detection. I work on it in my free time so development has been a little slow but I hope what I have can be of some use to someone!

  • 🌱 I’m currently learning C++. I hope to become proficient enough to one day contribute to the Godot game engine. The plan after C++ is to try and pick up Rust.

  • 🔥 I dislike web development. Well not really, I just don't get along with CSS sometimes.

  • 🎮 Favorite Games: Some of my favorite games are Bloodborne, Guilty Gear Strive, and .hack//G.U.

  • Fun fact: My username 'Pyxus' is just a combination of Pixel and Nexus. I came up with it back in middle school, it was supposed to be a clever way of saying "digital image".

🧰 Languages and Tools

C-Sharp

Java

Kotlin

JavaScript

TypeScript

Godot

Git

VS Code

Android Studio

React


📊 Stats

Anurag's GitHub stats

fray's People

Contributors

20milliliter avatar pyxus 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fray's Issues

FrayCombatStateMachine documentation doesn't adequately describe how to receive its data

The CombatStateMachine is intended to recieve inputs or sequences and automatically resolve the transition of states, but how is that data meant to be forwarded to other nodes, such as a fighter's AnimationPlayer?

FrayCombatStateMachine contains no signals.
FrayRootState has transitioned(from: StringName, to: StringName), but it is not referenced in any other classes and does not appear to be intended to be accessible from the state machine.

What is the intended method of allowing an AnimationPlayer (for instance) to change its animation in coordination with a state transition within a FrayCombatStateMachine?

Create example of how to implement contextual inputs.

As requested a example/tutorial on implementing contextual inputs should be created. An example of such input is the sprint/dodge button in souls games like Elden ring. When tapped the player enters a dodge state but if held the player enters a run state.

FrayCombatStateMachine.buffer_sequence not working as intended

Given the following FrayRootState:

var standing_state = FrayRootState.builder()\
	.transition_sequence("idle", "punch", {sequence="punch"})\
	.transition_sequence("idle", "kick", {sequence="kick"})\
	.start_at("idle")\
	#idle is returned to via a goto_start_state() call within AnimationPlayer animations
	.build()
	combat_state_machine.add_situation("standing_state", standing_state)

and the following print out of events:

Buffered punch into state_machine
Transitioned from idle to punch 
Buffered kick into state_machine
Transitioned from punch to idle

Intended behavior would be for a transition from idle to kick to occur thereafter, correct?

Add hit state creation utility

Problem

Creating hit states can be kind of tedious since you have to manually create a composition of a few different nodes. Namely HitState -> Hitbox -> CollisionShape. You can duplicate an exiting composition but you then have to go through and manually make each shape reference unique to avoid interfering with the original composition.

Solution

A quick create menu could potentially be added which can assist the user in adding new states allowing them to set how many hitboxes a state will have, what their attributes are, and what collision shape they use. The utility could also support copying existing state compositions while automatically making the shape resources unique.

Collision library bug

          I've found a small bug in the collision library. 
# hit_state_3d.gd
## Sets whether the hitbox at the given [kbd]index[/kbd] is active or not.
func set_hitbox_active(index: int, is_active: bool) -> void:
	var flag = 1 << index

	#if flag > active_hitboxes: 
		#push_warning("Index out of bounds.")
		#return
        # <-- Removed this warning since once all hit_box are disable with active_hitboxes = 0,
        #       it prevent me from re-activate them.

	if is_active:
		active_hitboxes |= flag
	else:
		active_hitboxes &= ~flag # <-- missing the ~

Originally posted by @Remi123 in #70 (comment)

Problems with sequence matcher

I'm currently building movement composites as follows:

func _ready():

	FrayInputMap.add_bind_action("left", "left")
	FrayInputMap.add_bind_action("right", "right")
	FrayInputMap.add_bind_action("up", "up")
	FrayInputMap.add_bind_action("down", "down")

	FrayInputMap.add_composite_input("forward", FrayConditionalInput.builder()
		.add_component("", FrayCombinationInput.builder()
			.add_component(FraySimpleInput.from_bind("right"))
			.mode_async()
			.build()
		)
		.add_component("on_right", FrayCombinationInput.builder()
			.add_component(FraySimpleInput.from_bind("left"))
			.mode_async()
			.is_virtual()
			.build()
		)
		.build()
	)

	FrayInputMap.add_composite_input("back", FrayConditionalInput.builder()
		.add_component("", FrayCombinationInput.builder()
			.add_component(FraySimpleInput.from_bind("left"))
			.mode_async()
			.build()
		)
		.add_component("on_right", FrayCombinationInput.builder()
			.add_component(FraySimpleInput.from_bind("right"))
			.mode_async()
			.is_virtual()
			.build()
		)
		.build()
	)

	FrayInputMap.add_composite_input("forward_up", FrayCombinationInput.builder()
		.add_component(FrayInputMap.get_composite_input("forward"))
		.add_component(FraySimpleInput.from_bind("up"))
		.mode_async()
		.is_virtual()
		.build()
	)

	FrayInputMap.add_composite_input("forward_down", FrayCombinationInput.builder()
		.add_component(FrayInputMap.get_composite_input("forward"))
		.add_component(FraySimpleInput.from_bind("down"))
		.mode_async()
		.is_virtual()
		.build()
	)

as well as simply displaying the FrayInputEvent from _on_FrayInput_input_detected.

This is the result:
sad

Sometimes the composite input will be ignored and only one of the components registered.
Of course, attempting to match sequences fails as erroneous inputs are being fed into the analyzer.

Is this a bug or is there a is_virtual() somewhere there shouldn't be?

Errors on import and enable

Attempting to import or enable fray into Godot Ver. rc1 returns errors.

Assuming the cause is rc1 having made breaking changes to Fray—and the most simple fix being downgrading—what version was used for the development of PR #24?

FrayCompoundState : change ready ordering for substate

Welcome back.

Didn't play too much with new features yet, but I appreciate the get_node function. Reduce my code size by a lot.

here is my change for the ready function :

func ready(context: Dictionary) -> void:
	if not context.is_read_only():
		context.make_read_only()
	_ready_impl(context) # <--- new emplacement
	for state_name in _states:
		var state: FrayState = _states[state_name]

		if state is FrayCompoundState:
			state.ready(context)
		else:
			state._ready_impl(context)
        #_ready_impl(context) # <--- Changed ordering

I use _ready_impl to create new states and keep things together in the code.

Substate are created in _ready_impl, then each of them are made ready afterward.

Thank you

Add editor tools for easier project setup and configuration

Hi, this is a nice addon.
But I wasn't unable to use it on my game, needs a proper tutorial to implement a basic input buffer.
Also, a good thing eventually would be a nice GUI editor tab for the project to easily set up stuff.

cool project.

Root state does not make it easy to interact with sub states

Currently there is no easy way to reference sub states. The obvious solution to me is to use a path based system. So rather than get_state(name_of_sub_state) you'll use get_state(path/to/state). It should be relatively simple to implement but considering the slow pace of development this can be put on hold for later.

Ugly exceptions on transition_sequence_global call

E 0:00:01:0062   root_state.gd:726 @ _create_global_transition(): Attempted to push_back an object into a TypedArray, that does not inherit from 'GDScript'.
  <C++ Error>    Condition "!other_script->inherits_script(script)" is true. Returning: false
  <C++ Source>   core/variant/container_type_validate.h:140 @ validate_object()
  <Stack Trace>  root_state.gd:726 @ _create_global_transition()
                 root_state.gd:646 @ transition_sequence_global()
                 ...

I would try to gather more information but I'm not exactly sure what the exception is trying to say.

Some scripts fail to compile on load

I'm not sure if I'm downloading the addon files wrong but whenever I try and add Fray to my project I get the following errors:
image
I get the same results when being added to a new project in both Godot 4.0 and 4.2

Building a State Machine documentation: Too few arguments on state_machine.state_changed.connect()

Given the following code on the documentation:

var state_machine: FrayStateMachine = $StateMachine

func _ready() -> void:
    state_machine.state_changed.connect()

func _process():
    if Input.is_action_just_pressed("ui_select"):
        state_machine.advance()

func _on_StateMachine_state_changed(from: StringName, to: StringName) -> void:
    print("State transitioned from '%s' to '%s'" % [from, to])

I receive the following error:

Too few arguments for "connect()" call. Expected at least 1 but received 0.

I also noticed receiving a error on _process() function for not having _delta argument and print_adj() for being a nonexistent function

[Bug?] CombatStateMachine's changing situation leave the previous one on the last state.

When you change situation, the new situation call goto_start(). However, when returning to the previous situation, the call to goto_start() is starting from the previous state when you left it when changing situation.

Example :
This is my states and situation

  • SituationA
    • StateA : Start state
    • StateB
  • SituationB
    • State1 : Start state
    • State2

Transition

  1. SituationA/StateB to Situation B
    SituationA is left in StateB. Current situation is SituationB/State1 due to goto_Start
  2. Coming back from situationB to SituationA
    SituationA call goto_start(), but since it was left in StateB, it transition from StateB to State A, calling StateB end_impl().

This can cause weird issues when the end_impl and start_impl is very important.

[TRACKER] Documentation Overhaul

Goal

This issue tracks progress of the ongoing documentation overhaul. The goal of this overhaul is to improve the readability, correctness, and usefulness of the current documentation including both doc-comments and markdown docs.

Contributing

If you would like to help me with the documentation overhaul simply create PR and leave a reply linking to it stating what it covers. This page will be updated as progress is made.

Doc-Comments

Doc-comment PRs will be divided by folders within a module. A '*' refers to every file within a folder

  • Collision: /* PR:
  • Collision: 2d/* PR:
  • Collision: 3d/* PR:
  • Input: /* PR:
  • Input: autoloads/* PR:
  • Input: device/binds/* PR:
  • Input: device/composites/* PR:
  • Input: events/* PR:
  • Input: sequence/* PR:
  • StateMgmt: /* PR:
  • StateMgmt: state/* PR:
  • StateMgmt: transition/* PR:
  • fray.gd PR:

Guides

The current "getting started" page is verbose and lacks clarity. Additionally, it focuses too much on 'how' to use Fray's tools (which is already covered by the doc-comments) but doesn't do a good job of addressing why they exist and what problems they solve. As such, I plan to replace that with a series of practical guides that explain how to solve common problems with Fray's tools.

Please share your thoughts if you have concerns with this approach or if you'd like to suggest/create guides.

  • README.md: Provides an overview of what fray is, what problems it solves / what features it has, and how to install.
  • Using Hitboxes: Covers the purpose of hitboxes, how to create them, and how to extend them using attributes.
  • Managing Multiple Hitboxes: Covers the the issue of managing multiple hitboxes, the purpose/use of hit states, and the purpose/use of hit state managers.
  • Detecting Inputs: Covers purpose/registration of binds, purpose/registration of composite inputs, checking inputs.
  • Detecting Input Sequences: Covers what a sequence is, use of sequence trees, purpose of negative edge, use of negative edge, use of sequence matcher.
  • Building A State Machine
  • Controlling State Transitions
  • Providing Data To States

Note: The present guide proposals are based on the existing getting started page. Since state management was refactored I need to re-evaluate what needs to be covered before I add state-related guides to the tracker.

Related Issues

#59
#44
#23

Enable Discussion in repo

Github has a settings for enabling the tab discussion. It allows for more general discussion instead of relying on issues to communicate.

Project -> Settings -> General -> Discussion. Toggle to enable.

Update Changelog

The changelog needs to be updated to accurately reflect all the changes that have occurred. In hindsight, I should have been doing incremental updates with each changelog reflecting those changes. However, I more or less forgot about it due to how far apart the updates have been (I wonder if there is a better approach than manually maintaining it). In any case the current log is inaccurate and I want to fix that.

RootState Transition At_End

Root state.gd lign 434

func _can_switch(transition: FrayStateMachineTransition) -> bool:
	return (
		transition.switch_mode == FrayStateMachineTransition.SwitchMode.IMMEDIATE
		or (transition.switch_mode == FrayStateMachineTransition.SwitchMode.AT_END 
		and is_done_processing())
		)

should be

func _can_switch(transition: FrayStateMachineTransition) -> bool:
	return (
		transition.switch_mode == FrayStateMachineTransition.SwitchMode.IMMEDIATE
		or (transition.switch_mode == FrayStateMachineTransition.SwitchMode.AT_END 
		and get_state_current()._is_done_processing_impl()) # <-------
		)

I based my correction on this enum in FrayStateMachineTransition :

enum SwitchMode{
	IMMEDIATE, ## Switch to the next state immediately.
	AT_END, ## Wait for the current state to end, then switch to the beginning of the next state.
}

The current implementation use the FrayRootState's _is_done_processing_impl() function, which basically check if we are at the end state of this root state. But FrayRootState didn't check the current state's _is_done_processing .

combat_sm.add_situation("grounded",FrayRootState.builder()
    .start_at('standing')
    .add_state('dash',dash_state) # custom FrayState that override _is_done_processing to wait for a timer on entering the state.
    .transition_sequence('standing','dash',{sequence="dash"}) # FraySequenceBranch for double tapping
    .transition("dash",'standing',{switch_mode = FrayStateMachineTransition.SwitchMode.AT_END,prereqs=[on_floor]})

I also changed root_state.gd:lign 430 for this

func _goto(to_state: StringName, args: Dictionary) -> void:
	if _ERR_INVALID_STATE(to_state): return

	var prev_state_name := _current_state
	var target_state: RefCounted = get_state(to_state) <----

	if target_state != null && not _current_state.is_empty(): <----
		get_state(_current_state)._exit_impl() <----

	_current_state = to_state
	get_state(_current_state)._enter_impl(args)
	transitioned.emit(prev_state_name, _current_state)

This correctly call _enter_impl() and _exit_impl(), previously it was exiting the target_state before entering it in any transition.

FrayInput: is_just_released function uses input_state.ispressed signal instead of input_state.is_pressed

In the else statement, input_state uses ispressed instead of is_pressed, causing a error

func is_just_released(input: StringName, device: int = DEVICE_KBM_JOY1) -> bool:
	match _get_input_state(input, device):
		var input_state:
			if Engine.is_in_physics_frame():
				return (
					not input_state.is_pressed
					and input_state.physics_frame == Engine.get_physics_frames()
				)
			else:
				return (
					not input_state.ispressed
					and input_state.process_frame == Engine.get_process_frames()
				)
		null:
			return false

Proposal: Add means of sharing data between states within a hierarchy.

Problem

Currently, sharing data among states within a hierarchy is cumbersome. If user-created states depend on specific information in order to function, then users are forced to manually provide this data during instantiation; this can result in a lot of boilerplate code.

Approach 1 - Read-only context propagating from root.

A possible approach to this is having a context dictionary which exists on the root. The dictionary would be read-only to prevent states from unpredictably modifying it. The dictionary would be passed to the State._ready_impl() method allowing user states to read from the context when added to the hierarchy.

Approach 2 - Shared mutable store that exists on the root.

Another approach is creating a shared store dictionary which exists on the root. The store would be private with methods available to read and update it.

Thoughts

Personally, I lean toward approach 1. Approach 2 would allow store values to be updated, which also facilitates a form of communication between states but I'm not entirely sure if that's useful or desirable. I can also see it being potentially error prone if states depend on reading from the store but the user isn't careful with how they modify it. Approach 1 avoids those issues by forcing the user to initialize the context with all the data their states require. Ignoring typos, if a state fails to fetch some data there is only one place that needs to be checked and that's when the context is initialized.

Update: It just occurred to me that if users truly wished to share mutable data between states in approach 1 they could easily provide an object all states interact with in the context. I further lean toward approach 1.

Neither approach is particularly difficult to implement. I'm open to hearing any thoughts on this including alternative solutions.

Building a State Machine documentation: Build a State Machine Root uses FrayCompoundstate.new() instead of FrayCompoundState.new()

In 2. Build State Machine Root, the root explicit configuration uses FrayCompoundstate instead of FrayCompoundState

# Explicit Configuration
var root := FrayCompoundstate.new()
root.add_state("a", FrayState.new())
root.add_state("b", FrayState.new())
root.add_state("c", FrayState.new())
root.add_transition("a", "b", FrayTransition.new())
root.add_transition("b", "c", FrayTransition.new())
root.add_transition("c", "a", FrayTransition.new())
root.start_state = "a"

# Builder Configuration
var root := (FrayCompoundState.builder()
    .start_at("a")
    .transition("a", "b")
    .transition("b", "c")
    .transition("c", "a")
    .build()
)

Rewrite Builders to take advantage 4.2 covariance

To make the builders more pleasant to work I would rewrite the builder per class despite each builder otherwise having identical functionality. This is because it was the only way to get autocomplete support for the methods unique to each builder. However, with the new return type covariance included in Godot 4.2 I can now remedy this. The downside is Fray will only support 4.2+ now, However I believe this is fine given the pending re-release.

Switched to using milliseconds for input related timings

It is common and useful in fighting games to refer to input and move timing in terms of frames. This should be the default representation of timing with an optional ms/sec representation.

Static methods to convert between frames and milliseconds were added. I now think it would make more sense for these times to be in milliseconds and users can choose to think in terms of frames if they so desire.

Godot hard crash due to FileSystem interaction with FrayCombatStateMachine

This is a weird one.

If there's a saved .tscn containing a FrayCombatStateMachine node that's currently open and CTRL+C is executed (despite this key combination being non-functional normally) within the FileSystem window, Godot hard crashes.

I'd be very surprised if there wasn't a larger engine bug at play here. Regardless, I wanted to check with you first.
Can you reproduce? You happen to have the slightest clue why this is happening?

Create new example project

The example project create for v1.x features sprites from the game Melty Blood Actress Again Current Code. I hadn't given it much thought when I originally shared the example but I don't feel I should be redistributing assets that aren't mine even if they're just for demonstration... I'll need to either find some assets I can share with this project or simplify the demonstration into something less playable such as just visualizing the state graph.

If anyone comes across this issue and would like to offer suggestions please do.

FrayHitboxAttribute function get_color() is "nonexistent"

Extending FrayHitBoxAttribute results in get_color() becoming inaccessible:

res://addons/fray/src/collision/2d/hitbox_2d.gd:113 - Invalid call. Nonexistent function 'get_color' in base 'Resource (ExtendedAttribute)'.

Redeclaring (/ "overriding") get_color() within the superclass body doesn't fix it either.

Fix discovered typos

  • Attribute is called Atrribute in several places
  • Some signals in the collision module use 'seperated' instead of 'separated'

[Bug] FrayInputBindJoyAxis doesn't return the correct strength

if I have this as a setup :

FrayInputMap.add_bind_joy_axis('left',JOY_AXIS_LEFT_X,false)
FrayInputMap.add_bind_joy_axis('right',JOY_AXIS_LEFT_X,true)
FrayInputMap.add_bind_joy_axis('down',JOY_AXIS_LEFT_Y,true)
FrayInputMap.add_bind_joy_axis('up',JOY_AXIS_LEFT_Y,false)

Then FrayInput.get_axis('left','right') return 0.0 most of the time.

After debugging, this function was the problem :

# FrayInputBindJoyAxis
func _get_strength_impl(device: int = 0) -> float:
	return Input.get_joy_axis(device, axis)

The axis variable is only distinct by negative value, so axis doesn't discern between left and right, so get_axis is effectively doing a 1 - 1 all the time.

I fixed this behavior with this :

# FrayInputBindJoyAxis
func _get_strength_impl(device: int = 0) -> float:
  var joy_axis := Input.get_joy_axis(device, axis)
  var is_positive_dir: bool = sign(joy_axis) == 1
  
  if abs(joy_axis) < deadzone or use_positive_axis != is_positive_dir:
    return 0.0
  
  return abs(joy_axis)

I've tested and now FrayInput.get_axis, FrayInput.get_strength, Input.get_axis and Input.get_action_strength always return the same results.

[Bug] CombatStateMachine::change_situation(...) call goto_start() twice

You can change situation in two ways in CombatStateMachine :

  1. csm.current_situation = 'on_ground'
  2. csm.change_situation('on_ground')

The current_situation variable has a setter that call goto_start(), and change_situation(...) call it again. The doc says to use the first option to change situation, but I saw that function and used it. It's used by add_situation but I don't see how changing it to the setter would change any behavior.

While I'm looking at this function, I think it would be nice to provide more option when changing situation. For now it leaves the previous situation in the same state without calling exit_impl(). I think something like this should be better

# Combat State Machine
func change_situation(situation_name: StringName, reset_previous_situation = false) -> void:
	if not has_situation(situation_name):
		push_error("Failed to change situation. State machine does not contain situation named '%s'" % situation_name)
		return

	if situation_name != current_situation:
		if(_root != null and reset_previous_situation ):
			_root._exit_impl()
			_root._current_state = ''
                       # There might be other things to do in order to reset correctly
		current_situation = situation_name
#		_root = get_situation(situation_name) // Already done by the setter
#		_root.goto_start() // Already done by the setter

GitHub and Godot downloads are broken.

When downloading Fray from GitHub the .zip file is only 176 B and contains nothing.

When downloading Fray from Godot it is only compatible with Godot 3.5.

Cloning is the only way of getting the current repo.

Rename FraySimpleStateMachine to FrayGeneralStateMachine

Simple has implications about the complexity of the state machine but really this is just a general purpose state machine. This also matches the naming scheme of FrayComplexStateMachine which is just a combat focused state machine.

FrayCombatStateMachine documentation doesn't adequately describe how to utilize conditions

Given FrayRootState construction:

var hit_state = FrayRootState.builder()\
	.transition("hit_low", "recovered", {
		"auto_advance" = true, advance_conditions = [FrayCondition.new("on_recovery")]
	})\
	.transition("hit_high", "recovered", {
		"auto_advance" = true, advance_conditions = [FrayCondition.new("on_recovery")]
	})\
.build()

The following warning is thrown when attempting to set the condition value:

combat_state_machine.get_root().set_condition("on_recovery", false)

state.gd:31 @ set_condition(): Condition '%s' does not exist

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.