diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7cee6e4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,465 @@ +# --------------------------------------------------------------------------- # +# Chickensoft C# Style — .editorconfig # +# --------------------------------------------------------------------------- # +# Godot-friendly, K&R coding style with a bit of Dart-style flair thrown in. # +# --------------------------------------------------------------------------- # +# # +# # +# ╓╗_▄╗_╓▄_ # +# ▄▄╟▓▓▓▓▓▓▓▓ # +# ╙▓▓▓▀▀╠╠╦╦╓,_ # +# ,φ╠╠╠╠╠╠╠╠╠╠▒╥ # +# φ╠╠╠╠╠╠╠╠╠╠╠╠╠╠╦ # +# @╠╠╫▌╠╟▌╠╠╠╠╠╠╠╠╠ # +# ╠╠╠▄▄▄▒╠╠╠╠╠╠╠╠╠╠b # +# ╠╠╨███▌╠╠╠╠╠╠╠▒╠╠▒_ ç╓ # +# ╠╠╠╠▒▒╠╠╠╠╠╠╠╠▒Å▄╠╬▒φ╩ε # +# ╚╠╠╠╠╠╠╠╠╠╠╠▒█▒╫█Å╠╠╩ # +# ╠╠╠╠╠╠╠╠╠╠╠╠╠╟╫▒╠╠╩ # +# ╙╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╜ # +# ╙╚╠╠╠╠╠╠╠╠╩╙ # +# ╒ µ # +# ▌ ▓ # +# ^▀▀ "▀ª # +# # +# # +# --------------------------------------------------------------------------- # +# +# Based on: +# - https://github.com/RehanSaeed/EditorConfig/blob/main/.editorconfig +# - https://gist.github.com/FaronBracy/155d8d7ad98b4ceeb526b9f47543db1b +# - various other gists floating around :) +# +# Have a problem? Encounter an issue? +# Come visit our Discord and let us know! https://discord.gg/MjA6HUzzAE +# +# Based on https://github.com/RehanSaeed/EditorConfig/blob/main/.editorconfig +# and https://gist.github.com/FaronBracy/155d8d7ad98b4ceeb526b9f47543db1b + +# This file is the top-most EditorConfig file +root = true + +# All Files +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +########################################## +# File Extension Settings +########################################## + +# Visual Studio Solution Files +[*.sln] +indent_style = tab + +# Visual Studio XML Project Files +[*.{csproj,vbproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML Configuration Files +[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] +indent_size = 2 + +# JSON Files +[*.{json,json5,webmanifest}] +indent_size = 2 + +# YAML Files +[*.{yml,yaml}] +indent_size = 2 + +# Markdown Files +[*.{md,mdx}] +trim_trailing_whitespace = false +max_line_length = off # use soft wrapping in editor + +# Web Files +[*.{htm,html,js,jsm,ts,tsx,cjs,cts,ctsx,mjs,mts,mtsx,css,sass,scss,less,pcss,svg,vue}] +indent_size = 2 + +# Batch Files +[*.{cmd,bat}] +end_of_line = crlf + +# Bash Files +[*.sh] +end_of_line = lf + +# Makefiles +[Makefile] +indent_style = tab + +[*{._Generated.cs,.g.cs,.generated.cs}] +# Ignore a lack of documentation for generated code. Doesn't apply to builds, +# just to viewing generation output. +dotnet_diagnostic.CS1591.severity = none + +########################################## +# Default .NET Code Style Severities +########################################## + +[*.{cs,csx,cake,vb,vbx}] +# Default Severity for all .NET Code Style rules below +dotnet_analyzer_diagnostic.severity = warning + +########################################## +# Language Rules +########################################## + +# .NET Style Rules + +# "this." and "Me." qualifiers +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_property = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_event = false + +# Language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = always:warning +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:warning +visual_basic_preferred_modifier_order = Partial, Default, Private, Protected, Public, Friend, NotOverridable, Overridable, MustOverride, Overloads, Overrides, MustInherit, NotInheritable, Static, Shared, Shadows, ReadOnly, WriteOnly, Dim, Const, WithEvents, Widening, Narrowing, Custom, Async:warning +dotnet_style_readonly_field = true:warning + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning + +# Expression-level preferences +dotnet_style_object_initializer = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_prefer_inferred_tuple_names = true:warning +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_diagnostic.IDE0045.severity = suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_diagnostic.IDE0046.severity = suggestion +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_simplified_interpolation = true:warning +dotnet_style_prefer_simplified_boolean_expressions = true:warning + +# Null-checking preferences +dotnet_style_coalesce_expression = true:warning +dotnet_style_null_propagation = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning + +# File header preferences +# Keep operators at end of line when wrapping. +dotnet_style_operator_placement_when_wrapping = end_of_line +csharp_style_prefer_null_check_over_type_check = true:warning + +# Code block preferences +csharp_prefer_braces = true:warning +csharp_prefer_simple_using_statement = true:suggestion +dotnet_diagnostic.IDE0063.severity = suggestion + +# C# Style Rules +[*.{cs,csx,cake}] +# 'var' preferences +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_style_var_elsewhere = true:warning +# Expression-bodied members +csharp_style_expression_bodied_methods = true:warning +csharp_style_expression_bodied_constructors = false:warning +csharp_style_expression_bodied_operators = true:warning +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_lambdas = true:warning +csharp_style_expression_bodied_local_functions = true:warning +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_prefer_switch_expression = true:warning +csharp_style_prefer_pattern_matching = true:warning +csharp_style_prefer_not_pattern = true:warning +# Expression-level preferences +csharp_style_inlined_variable_declaration = true:warning +csharp_prefer_simple_default_expression = true:warning +csharp_style_pattern_local_over_anonymous_function = true:warning +csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_range_operator = true:warning +csharp_style_implicit_object_creation_when_type_is_apparent = true:warning +# "Null" checking preferences +csharp_style_throw_expression = true:warning +csharp_style_conditional_delegate_call = true:warning +# Code block preferences +csharp_prefer_braces = true:warning +csharp_prefer_simple_using_statement = true:suggestion +dotnet_diagnostic.IDE0063.severity = suggestion +# 'using' directive preferences +csharp_using_directive_placement = inside_namespace:warning +# Modifier preferences +# Don't suggest making public methods static. Very annoying. +csharp_prefer_static_local_function = false +# Only suggest making private methods static (if they don't use instance data). +dotnet_code_quality.CA1822.api_surface = private + +########################################## +# Unnecessary Code Rules +########################################## + +# .NET Unnecessary code rules + +dotnet_code_quality_unused_parameters = non_public:suggestion +dotnet_remove_unnecessary_suppression_exclusions = none +dotnet_diagnostic.IDE0079.severity = warning + +# C# Unnecessary code rules + +# Chickensoft Unused Code Additions +# +# Unfortunately for VSCode users, disabling these rules prevents you from +# detecting unused code. Enabling them will trigger the roslyn analyzers' +# automatic code fixes to remove unused code, which is very annoying when +# you're actively coding or using reflection. +# +# I have not found a way to disable automatic fixes while keeping +# warnings/suggestions/etc in the editor. If you find a way, please file an +# issue or open a PR. + +# Don't remove method parameters that are unused. +dotnet_diagnostic.IDE0060.severity = none +dotnet_diagnostic.RCS1163.severity = none + +# Don't remove methods that are unused. +dotnet_diagnostic.IDE0051.severity = none +dotnet_diagnostic.RCS1213.severity = none + +# Use discard variable for unused expression values. +csharp_style_unused_value_expression_statement_preference = discard_variable + +# .NET formatting rules + +# Organize using directives +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# Dotnet namespace options +# +# We don't care about namespaces matching folder structure. Games and apps +# are complicated and you are free to organize them however you like. Change +# this if you want to enforce it. +dotnet_style_namespace_match_folder = false +dotnet_diagnostic.IDE0130.severity = none + +# C# formatting rules + +# Newline options +csharp_new_line_before_open_brace = none +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation options +csharp_indent_switch_labels = true +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = no_change +csharp_indent_block_contents = true +csharp_indent_braces = false + +# Spacing options +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_comma = true +csharp_space_before_comma = false +csharp_space_after_dot = false +csharp_space_before_dot = false +csharp_space_after_semicolon_in_for_statement = true +csharp_space_before_semicolon_in_for_statement = false +csharp_space_around_declaration_statements = false +csharp_space_before_open_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_square_brackets = false + +# Wrap options +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true + +# Namespace options +csharp_style_namespace_declarations = file_scoped:warning + +########################################## +# .NET Naming Rules +########################################## + +########################################## +# Chickensoft Naming Conventions & Styles +# These deviate heavily from Microsoft's Official Naming Conventions. +########################################## + +# Allow underscores in names. +dotnet_diagnostic.CA1707.severity = none + +# Styles +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +dotnet_naming_style.upper_case_style.capitalization = all_upper +dotnet_naming_style.upper_case_style.word_separator = _ + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Use uppercase for all constant fields. +dotnet_naming_rule.constants_uppercase.severity = suggestion +dotnet_naming_rule.constants_uppercase.symbols = constant_fields +dotnet_naming_rule.constants_uppercase.style = upper_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const + +# Non-public fields should be _camelCase +dotnet_naming_rule.non_public_fields_under_camel.severity = suggestion +dotnet_naming_rule.non_public_fields_under_camel.symbols = non_public_fields +dotnet_naming_rule.non_public_fields_under_camel.style = camel_case_underscore_style +dotnet_naming_symbols.non_public_fields.applicable_kinds = field +dotnet_naming_symbols.non_public_fields.required_modifiers = +dotnet_naming_symbols.non_public_fields.applicable_accessibilities = private, private_protected, protected, internal, protected, protected_internal + +# Public fields should be PascalCase +dotnet_naming_rule.public_fields_pascal.severity = suggestion +dotnet_naming_rule.public_fields_pascal.symbols = public_fields +dotnet_naming_rule.public_fields_pascal.style = pascal_case_style +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.required_modifiers = +dotnet_naming_symbols.public_fields.applicable_accessibilities = public + +# Async methods should have "Async" suffix. +# Disabled because it makes tests too verbose. +# dotnet_naming_style.end_in_async.required_suffix = Async +# dotnet_naming_style.end_in_async.capitalization = pascal_case +# dotnet_naming_rule.methods_end_in_async.symbols = methods_async +# dotnet_naming_rule.methods_end_in_async.style = end_in_async +# dotnet_naming_rule.methods_end_in_async.severity = warning +# dotnet_naming_symbols.methods_async.applicable_kinds = method +# dotnet_naming_symbols.methods_async.required_modifiers = async +# dotnet_naming_symbols.methods_async.applicable_accessibilities = * + +########################################## +# Other Naming Rules +########################################## + +# All of the following must be PascalCase: +dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property +dotnet_naming_rule.element_rule.symbols = element_group +dotnet_naming_rule.element_rule.style = pascal_case_style +dotnet_naming_rule.element_rule.severity = warning + +# Interfaces use PascalCase and are prefixed with uppercase 'I' +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case +dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I +dotnet_naming_symbols.interface_group.applicable_kinds = interface +dotnet_naming_rule.interface_rule.symbols = interface_group +dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style +dotnet_naming_rule.interface_rule.severity = warning + +# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case +dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T +dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter +dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group +dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style +dotnet_naming_rule.type_parameter_rule.severity = warning + +# Function parameters use camelCase +# https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters +dotnet_naming_symbols.parameters_group.applicable_kinds = parameter +dotnet_naming_rule.parameters_rule.symbols = parameters_group +dotnet_naming_rule.parameters_rule.style = camel_case_style +dotnet_naming_rule.parameters_rule.severity = warning + +# Anything not specified uses camel case. +dotnet_naming_rule.unspecified_naming.severity = warning +dotnet_naming_rule.unspecified_naming.symbols = unspecified +dotnet_naming_rule.unspecified_naming.style = camel_case_style +dotnet_naming_symbols.unspecified.applicable_kinds = * +dotnet_naming_symbols.unspecified.applicable_accessibilities = * + +########################################## +# Chickensoft Rule Overrides +########################################## + +# Allow using keywords as names +# dotnet_diagnostic.CA1716.severity = none +# Don't require culture info for ToString() +dotnet_diagnostic.CA1304.severity = none +# Don't require a string comparison for comparing strings. +dotnet_diagnostic.CA1310.severity = none +# Don't require a string format specifier. +dotnet_diagnostic.CA1305.severity = none +# Allow protected fields. +dotnet_diagnostic.CA1051.severity = none +# Don't warn about checking values that are supposedly never null. Sometimes +# they are actually null. +dotnet_diagnostic.CS8073.severity = none +# Don't remove seemingly "unnecessary" assignments, as they often have +# intended side-effects. +dotnet_diagnostic.IDE0059.severity = none +# Switch/case should always have a default clause. Tell that to Roslynator. +dotnet_diagnostic.RCS1070.severity = none +# Tell roslynator not to eat unused parameters. +dotnet_diagnostic.RCS1163.severity = none +# Tell dotnet not to remove unused parameters. +dotnet_diagnostic.IDE0060.severity = none +# Tell roslynator not to remove `partial` modifiers. +dotnet_diagnostic.RCS1043.severity = none +# Tell roslynator not to make classes static so aggressively. +dotnet_diagnostic.RCS1102.severity = none +# Roslynator wants to make properties readonly all the time, so stop it. +# The developer knows best when it comes to contract definitions with Godot. +dotnet_diagnostic.RCS1170.severity = none +# Allow expression values to go unused, even without discard variable. +# Otherwise, using Moq would be way too verbose. +dotnet_diagnostic.IDE0058.severity = none +# Don't let roslynator turn every local variable into a const. +# If we did, we'd have to specify the types of local variables far more often, +# and this style prefers type inference. +dotnet_diagnostic.RCS1118.severity = none +# Enums don't need to declare explicit values. Everyone knows they start at 0. +dotnet_diagnostic.RCS1161.severity = none +# Allow unconstrained type parameter to be checked for null. +dotnet_diagnostic.RCS1165.severity = none +# Allow keyword-based names so that parameter names like `@event` can be used. +dotnet_diagnostic.CA1716.severity = none +# Let me put comments where I like +dotnet_diagnostic.RCS1181.severity = none +# Allow me to use the word Collection if I want. +dotnet_diagnostic.CA1711.severity = none +# No primary constructors — not supported well by tooling. +dotnet_diagnostic.IDE0290.severity = none +# Let me write dumb if statements for readability. +dotnet_diagnostic.IDE0046.severity = none +# Don't make me use expression bodies for methods +dotnet_diagnostic.IDE0022.severity = none +# Don't make me populate a switch expression redundantly +dotnet_diagnostic.IDE0072.severity = none diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3b7ca90 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,46 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf + +# Image formats +*.bmp filter=lfs diff=lfs merge=lfs -text +*.dds filter=lfs diff=lfs merge=lfs -text +*.exr filter=lfs diff=lfs merge=lfs -text +*.hdr filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.tga filter=lfs diff=lfs merge=lfs -text +*.svg filter=lfs diff=lfs merge=lfs -text +*.webp filter=lfs diff=lfs merge=lfs -text + +# Audio and Video formats +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.wav filter=lfs diff=lfs merge=lfs -text +*.ogg filter=lfs diff=lfs merge=lfs -text +*.ogx filter=lfs diff=lfs merge=lfs -text +*.ogv filter=lfs diff=lfs merge=lfs -text + +# 3D formats +*.gltf filter=lfs diff=lfs merge=lfs -text +*.blend filter=lfs diff=lfs merge=lfs -text +*.blend1 filter=lfs diff=lfs merge=lfs -text +*.glb filter=lfs diff=lfs merge=lfs -text +*.dae filter=lfs diff=lfs merge=lfs -text +*.obj filter=lfs diff=lfs merge=lfs -text +*.fbx filter=lfs diff=lfs merge=lfs -text + +# Build +*.dll filter=lfs diff=lfs merge=lfs -text +*.exe filter=lfs diff=lfs merge=lfs -text +*.pdb filter=lfs diff=lfs merge=lfs -text +*.so filter=lfs diff=lfs merge=lfs -text +*.dylib filter=lfs diff=lfs merge=lfs -text + +# Packaging +*.zip filter=lfs diff=lfs merge=lfs -text +*.7z filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.file filter=lfs diff=lfs merge=lfs -text +*.dylib filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/auto_release.yaml b/.github/workflows/auto_release.yaml new file mode 100644 index 0000000..5209f30 --- /dev/null +++ b/.github/workflows/auto_release.yaml @@ -0,0 +1,81 @@ +# This workflow will run whenever tests finish running. If tests pass, it will +# look at the last commit message to see if it contains the phrase +# "chore(deps): update all dependencies". +# +# If it finds a commit with that phrase, and the testing workflow has passed, +# it will automatically release a new version of the project by running the +# publish workflow. +# +# The commit message phrase above is always used by renovatebot when opening +# PR's to update dependencies. If you have renovatebot enabled and set to +# automatically merge in dependency updates, this can automatically release and +# publish the updated version of the project. +# +# You can disable this action by setting the DISABLE_AUTO_RELEASE repository +# variable to true. + +name: '🦾 Auto-Release' +on: + workflow_run: + workflows: ["🚥 Tests"] + branches: + - main + types: + - completed + +jobs: + auto_release: + name: 🦾 Auto-Release + runs-on: ubuntu-latest + outputs: + should_release: ${{ steps.release.outputs.should_release }} + steps: + - name: 🧾 Checkout + uses: actions/checkout@v4 + with: + # Use your GitHub Personal Access Token variable here. + token: ${{ secrets.GH_BASIC }} + lfs: true + submodules: 'recursive' + + - name: 🧑‍🔬 Check Test Results + id: tests + run: | + echo "passed=${{ github.event.workflow_run.conclusion == 'success' }}" >> "$GITHUB_OUTPUT" + + - name: 📄 Check If Dependencies Changed + id: deps + run: | + message=$(git log -1 --pretty=%B) + + if [[ $message == *"chore(deps)"* ]]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + + - name: 📝 Check Release Status + id: release + run: | + echo "Tests passed: ${{ steps.tests.outputs.passed }}" + echo "Dependencies changed: ${{ steps.deps.outputs.changed }}" + disable_auto_release='${{ vars.DISABLE_AUTO_RELEASE }}' + echo "DISABLE_AUTO_RELEASE=$disable_auto_release" + + if [[ ${{ steps.tests.outputs.passed }} == "true" && ${{ steps.deps.outputs.changed }} == "true" && $disable_auto_release != "true" ]]; then + echo "should_release=true" >> "$GITHUB_OUTPUT" + echo "🦾 Creating a release!" + else + echo "should_release=false" >> "$GITHUB_OUTPUT" + echo "✋ Not creating a release." + fi + + trigger_release: + uses: './.github/workflows/release.yaml' + needs: auto_release + if: needs.auto_release.outputs.should_release == 'true' + secrets: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + GH_BASIC: ${{ secrets.GH_BASIC }} + with: + bump: patch diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..0214af4 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,99 @@ +name: "📦 Release" +on: + # Make a release whenever the developer wants. + workflow_dispatch: + inputs: + bump: + type: string + description: "major, minor, or patch" + required: true + default: "patch" + # Make a release whenever we're told to by another workflow. + workflow_call: + secrets: + NUGET_API_KEY: + description: "API key for Nuget" + required: true + GH_BASIC: + description: "Personal access token (PAT) for GitHub" + required: true + # Input unifies with the workflow dispatch since it's identical. + inputs: + bump: + type: string + description: "major, minor, or patch" + required: true + default: "patch" +jobs: + release: + name: "📦 Release" + runs-on: ubuntu-latest + env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_NOLOGO: true + steps: + - name: 🧾 Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_BASIC }} + lfs: true + submodules: "recursive" + fetch-depth: 0 # So we can get all tags. + + - name: 🔎 Read Current Project Version + id: current-version + uses: WyriHaximus/github-action-get-previous-tag@v1 + with: + fallback: "0.0.0-devbuild" + + - name: 🖨 Print Current Version + run: | + echo "Current Version: ${{ steps.current-version.outputs.tag }}" + + - name: 🧮 Compute Next Version + uses: chickensoft-games/next-godot-csproj-version@v1 + id: next-version + with: + project-version: ${{ steps.current-version.outputs.tag }} + godot-version: global.json + bump: ${{ inputs.bump }} + + - uses: actions/setup-dotnet@v4 + name: 💽 Setup .NET SDK + with: + # Use the .NET SDK from global.json in the root of the repository. + global-json-file: global.json + + # Write version to file so .NET will build correct version. + - name: 📝 Write Version to File + uses: jacobtomlinson/gha-find-replace@v3 + with: + find: "0.0.0-devbuild" + replace: ${{ steps.next-version.outputs.version }} + regex: false + include: Chickensoft.PalettePainter/Chickensoft.PalettePainter.csproj + + - name: 📦 Build + run: dotnet build -c Release + working-directory: Chickensoft.PalettePainter + + - name: 🔎 Get Package Path + id: package-path + run: | + package=$(find ./Chickensoft.PalettePainter/nupkg -name "*.nupkg") + echo "package=$package" >> "$GITHUB_OUTPUT" + echo "📦 Found package: $package" + + - name: ✨ Create Release + env: + GITHUB_TOKEN: ${{ secrets.GH_BASIC }} + run: | + version="${{ steps.next-version.outputs.version }}" + gh release create --title "v$version" --generate-notes "$version" \ + "${{ steps.package-path.outputs.package }}" + + - name: 🛜 Publish to Nuget + run: | + dotnet nuget push "${{ steps.package-path.outputs.package }}" \ + --api-key "${{ secrets.NUGET_API_KEY }}" \ + --source "https://api.nuget.org/v3/index.json" --skip-duplicate diff --git a/.github/workflows/spellcheck.yaml b/.github/workflows/spellcheck.yaml new file mode 100644 index 0000000..c7ca58f --- /dev/null +++ b/.github/workflows/spellcheck.yaml @@ -0,0 +1,26 @@ +name: '🧑‍🏫 Spellcheck' +on: + push: + pull_request: + +jobs: + spellcheck: + name: '🧑‍🏫 Spellcheck' + # Only run the workflow if it's not a PR or if it's a PR from a fork. + # This prevents duplicate workflows from running on PR's that originate + # from the repository itself. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + defaults: + run: + working-directory: '.' + steps: + - uses: actions/checkout@v4 + name: 🧾 Checkout + + - uses: streetsidesoftware/cspell-action@v6 + name: 📝 Check Spelling + with: + config: './cspell.json' + incremental_files_only: false + root: '.' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..377ed34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +**/coverage/* +!**/coverage/.gdignore +nupkg/ +.godot/ +bin/ +obj/ +.generated/ +.vs/ +.DS_Store +*.DotSettings.user +*.binlog diff --git a/.idea/.idea.Chickensoft.PalettePainter/.idea/.gitignore b/.idea/.idea.Chickensoft.PalettePainter/.idea/.gitignore new file mode 100644 index 0000000..6c78d2d --- /dev/null +++ b/.idea/.idea.Chickensoft.PalettePainter/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/.idea.Chickensoft.PalettePainter.iml +/modules.xml +/projectSettingsUpdater.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.Chickensoft.PalettePainter/.idea/.name b/.idea/.idea.Chickensoft.PalettePainter/.idea/.name new file mode 100644 index 0000000..4030062 --- /dev/null +++ b/.idea/.idea.Chickensoft.PalettePainter/.idea/.name @@ -0,0 +1 @@ +Chickensoft.PalettePainter \ No newline at end of file diff --git a/.idea/.idea.Chickensoft.PalettePainter/.idea/codeStyles/codeStyleConfig.xml b/.idea/.idea.Chickensoft.PalettePainter/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/.idea.Chickensoft.PalettePainter/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.Chickensoft.PalettePainter/.idea/encodings.xml b/.idea/.idea.Chickensoft.PalettePainter/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.Chickensoft.PalettePainter/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.Chickensoft.PalettePainter/.idea/indexLayout.xml b/.idea/.idea.Chickensoft.PalettePainter/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.Chickensoft.PalettePainter/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Chickensoft.PalettePainter/.idea/vcs.xml b/.idea/.idea.Chickensoft.PalettePainter/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.Chickensoft.PalettePainter/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.run/Chickensoft.PalettePainter.run.xml b/.run/Chickensoft.PalettePainter.run.xml new file mode 100644 index 0000000..4e6a8bc --- /dev/null +++ b/.run/Chickensoft.PalettePainter.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ac45447 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "ms-dotnettools.csharp", + "selcukermaya.se-csproj-extensions", + "josefpihrt-vscode.roslynator", + "streetsidesoftware.code-spell-checker", + "VisualStudioExptTeam.vscodeintellicode", + "DavidAnson.vscode-markdownlint" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..008163e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,37 @@ +{ + "version": "0.2.0", + "configurations": [ + // For these launch configurations to work, you need to setup a GODOT + // environment variable. On mac or linux, this can be done by adding + // the following to your .zshrc, .bashrc, or .bash_profile file: + // export GODOT="/Applications/Godot.app/Contents/MacOS/Godot" + { + "name": "🧪 Debug Tests", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${env:GODOT}", + "args": [ + // These command line flags are used by GoDotTest to run tests. + "--run-tests", + "--quit-on-finish" + ], + "cwd": "${workspaceFolder}/Chickensoft.PalettePainter.Tests", + "stopAtEntry": false, + }, + { + "name": "🔬 Debug Current Test", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${env:GODOT}", + "args": [ + // These command line flags are used by GoDotTest to run tests. + "--run-tests=${fileBasenameNoExtension}", + "--quit-on-finish" + ], + "cwd": "${workspaceFolder}/Chickensoft.PalettePainter.Tests", + "stopAtEntry": false, + }, + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..863b15d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,169 @@ +{ + "[csharp]": { + "editor.codeActionsOnSave": { + "source.addMissingImports": "explicit", + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.formatOnType": false + }, + "csharp.semanticHighlighting.enabled": true, + "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true, + // Required to keep the C# language server from getting confused about which + // solution to open. + "dotnet.defaultSolution": "Chickensoft.Godotpackage.sln", + "dotnet.server.useOmnisharp": false, + "editor.semanticHighlighting.enabled": true, + // C# doc comment colorization gets lost with semantic highlighting, but we + // need semantic highlighting for proper syntax highlighting with record + // shorthand. + // + // Here's a workaround for doc comment highlighting from + // https://github.com/OmniSharp/omnisharp-vscode/issues/3816 + "editor.tokenColorCustomizations": { + "[*]": { + // Themes that don't include the word "Dark" or "Light" in them. + // These are some bold colors that show up well against most dark and + // light themes. + // + // Change them to something that goes well with your preferred theme :) + "textMateRules": [ + { + "scope": "comment.documentation", + "settings": { + "foreground": "#0091ff" + } + }, + { + "scope": "comment.documentation.attribute", + "settings": { + "foreground": "#8480ff" + } + }, + { + "scope": "comment.documentation.cdata", + "settings": { + "foreground": "#0091ff" + } + }, + { + "scope": "comment.documentation.delimiter", + "settings": { + "foreground": "#aa00ff" + } + }, + { + "scope": "comment.documentation.name", + "settings": { + "foreground": "#ef0074" + } + } + ] + }, + "[*Dark*]": { + // Themes that include the word "Dark" in them. + "textMateRules": [ + { + "scope": "comment.documentation", + "settings": { + "foreground": "#608B4E" + } + }, + { + "scope": "comment.documentation.attribute", + "settings": { + "foreground": "#C8C8C8" + } + }, + { + "scope": "comment.documentation.cdata", + "settings": { + "foreground": "#E9D585" + } + }, + { + "scope": "comment.documentation.delimiter", + "settings": { + "foreground": "#808080" + } + }, + { + "scope": "comment.documentation.name", + "settings": { + "foreground": "#569CD6" + } + } + ] + }, + "[*Light*]": { + // Themes that include the word "Light" in them. + "textMateRules": [ + { + "scope": "comment.documentation", + "settings": { + "foreground": "#008000" + } + }, + { + "scope": "comment.documentation.attribute", + "settings": { + "foreground": "#282828" + } + }, + { + "scope": "comment.documentation.cdata", + "settings": { + "foreground": "#808080" + } + }, + { + "scope": "comment.documentation.delimiter", + "settings": { + "foreground": "#808080" + } + }, + { + "scope": "comment.documentation.name", + "settings": { + "foreground": "#808080" + } + } + ] + } + }, + "markdownlint.config": { + // Allow non-unique heading names so we don't break the changelog. + "MD024": false, + // Allow html in markdown. + "MD033": false + }, + "markdownlint.lintWorkspaceGlobs": [ + "!**/LICENSE" + ], + "omnisharp.enableEditorConfigSupport": true, + "omnisharp.enableMsBuildLoadProjectsOnDemand": false, + "omnisharp.maxFindSymbolsItems": 3000, + "omnisharp.organizeImportsOnFormat": true, + "omnisharp.useModernNet": true, + // Remove these if you're happy with your terminal profiles. + "terminal.integrated.defaultProfile.windows": "Git Bash", + "terminal.integrated.profiles.windows": { + "Command Prompt": { + "icon": "terminal-cmd", + "path": [ + "${env:windir}\\Sysnative\\cmd.exe", + "${env:windir}\\System32\\cmd.exe" + ] + }, + "Git Bash": { + "icon": "terminal", + "source": "Git Bash" + }, + "PowerShell": { + "icon": "terminal-powershell", + "source": "PowerShell" + } + } +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..4230088 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,40 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "--no-restore" + ], + "problemMatcher": "$msCompile", + "presentation": { + "echo": true, + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false + } + }, + { + "label": "coverage", + "group": "test", + "command": "${workspaceFolder}/Chickensoft.PalettePainter.Tests/coverage.sh", + "type": "shell", + "options": { + "cwd": "${workspaceFolder}/Chickensoft.PalettePainter.Tests" + }, + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": true + }, + }, + ] +} \ No newline at end of file diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..fe827dd --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,7 @@ +{ + "languages": { + "Markdown": { + "soft_wrap": "bounded" + } + } +} diff --git a/.zed/tasks.json b/.zed/tasks.json new file mode 100644 index 0000000..fc4c2c2 --- /dev/null +++ b/.zed/tasks.json @@ -0,0 +1,16 @@ +[ + { + "label": "Run PalettePainter", + "command": "dotnet", + "reveal": "always", + "args": [ + "run", + "--project", + "Chickensoft.PalettePainter/Chickensoft.PalettePainter.csproj", + "generate", + "output.png", + "-x", + "12" + ] + } +] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0f84802 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contributing + +Thank you for taking the time to read this contributing guide and for showing interest in helping this project! + +## Getting Started + +Need a helping hand to get started? Check out these resources! + +- [Discord Server][discord] +- [Chickensoft Website][chickensoft] + +Please read our [code of conduct](#code-of-conduct). We do our best to treat others fairly and foster a welcoming environment. + +## Project Setup + +This is a C# nuget package, for use with the .NET SDK 6 or 7. As such, the `dotnet` tool will allow you to restore packages and build projects. + +The `Chickensoft.PalettePainter.Tests` project must be built with the Godot editor at least once before `dotnet build` will succeed. Godot has to generate the .NET bindings for the project, since tests run in an actual game environment. + +## Coding Guidelines + +Your IDE should automatically adhere to the style guidelines in the provided `.editorconfig` file. Please try to keep lines under 80 characters long whenever possible. + +We try to write tests for our projects to ensure a certain level of quality. We are willing to give you support and guidance if you need help! + +## Code of Conduct + +We follow the [Contributor Covenant][covenant]. + +In short: + +> We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + + + +[discord]: https://discord.gg/gSjaPgMmYW +[chickensoft]: https://chickensoft.games +[covenant]: https://www.contributor-covenant.org/version/2/1/code_of_conduct/ diff --git a/Chickensoft.PalettePainter.Tests/Chickensoft.PalettePainter.Tests.csproj b/Chickensoft.PalettePainter.Tests/Chickensoft.PalettePainter.Tests.csproj new file mode 100644 index 0000000..9c5b30a --- /dev/null +++ b/Chickensoft.PalettePainter.Tests/Chickensoft.PalettePainter.Tests.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + diff --git a/Chickensoft.PalettePainter.Tests/UnitTest1.cs b/Chickensoft.PalettePainter.Tests/UnitTest1.cs new file mode 100644 index 0000000..1857d34 --- /dev/null +++ b/Chickensoft.PalettePainter.Tests/UnitTest1.cs @@ -0,0 +1,6 @@ +namespace Chickensoft.PalettePainter.Tests; + +public class UnitTest1 { + [Fact] + public void Test1() { } +} diff --git a/Chickensoft.PalettePainter.sln b/Chickensoft.PalettePainter.sln new file mode 100644 index 0000000..6f7b6de --- /dev/null +++ b/Chickensoft.PalettePainter.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chickensoft.PalettePainter", "Chickensoft.PalettePainter\Chickensoft.PalettePainter.csproj", "{968B4768-1F08-4E47-AD54-23DC06A2BB80}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chickensoft.PalettePainter.Tests", "Chickensoft.PalettePainter.Tests\Chickensoft.PalettePainter.Tests.csproj", "{D4C71ACA-462E-4240-A8A3-06144B678A83}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {968B4768-1F08-4E47-AD54-23DC06A2BB80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {968B4768-1F08-4E47-AD54-23DC06A2BB80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {968B4768-1F08-4E47-AD54-23DC06A2BB80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {968B4768-1F08-4E47-AD54-23DC06A2BB80}.Release|Any CPU.Build.0 = Release|Any CPU + {D4C71ACA-462E-4240-A8A3-06144B678A83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4C71ACA-462E-4240-A8A3-06144B678A83}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4C71ACA-462E-4240-A8A3-06144B678A83}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4C71ACA-462E-4240-A8A3-06144B678A83}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Chickensoft.PalettePainter/Chickensoft.PalettePainter.csproj b/Chickensoft.PalettePainter/Chickensoft.PalettePainter.csproj new file mode 100644 index 0000000..c7a0d42 --- /dev/null +++ b/Chickensoft.PalettePainter/Chickensoft.PalettePainter.csproj @@ -0,0 +1,57 @@ + + + Exe + true + palettepainter + net8.0 + Major + true + preview + true + enable + true + Chickensoft.PalettePainter + true + ./nupkg + portable + + PalettePainter + 0.0.0 + Command-line, general-purpose palette generator for use with pixel art, textures, or art software. + © 2024 Your Name + Your Name + Your Name + + Chickensoft.PalettePainter + Chickensoft.PalettePainter release. + icon.png + palette;pixel art;art;generator;command line;cli;chickensoft;utils + README.md + LICENSE + https://github.com/chickensoft-games/PalettePainter + + git + https://github.com/chickensoft-games/PalettePainter.git + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/Chickensoft.PalettePainter/icon.png b/Chickensoft.PalettePainter/icon.png new file mode 100644 index 0000000..a901557 --- /dev/null +++ b/Chickensoft.PalettePainter/icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:324496ebf5f67bf30ef6090d609fb16277e736bc883b29d734a9bfa79c2779d4 +size 86076 diff --git a/Chickensoft.PalettePainter/src/Colors.cs b/Chickensoft.PalettePainter/src/Colors.cs new file mode 100644 index 0000000..8391bb8 --- /dev/null +++ b/Chickensoft.PalettePainter/src/Colors.cs @@ -0,0 +1,83 @@ +namespace Chickensoft.PalettePainter; + +using System; +using SkiaSharp; + +public static class Colors { + /// + /// Given an interval between 0 and 1, return the saturation level for + /// the color at that interval along the ramp (0-100). + /// This was determined from using polynomial regression based on the values + /// provided by slynyrd. + /// https://www.slynyrd.com/blog/2018/1/10/pixelblog-1-color-palettes + /// + /// + /// Color position along the ramp as a value between 0 + /// and 1. + /// + /// Saturation value between 0 and 100. + public static double SlynyrdSaturationFunction(double interval) => + Math.Clamp(20 + (169 * interval) + (257 * Math.Pow(interval, 2)) + + (-1706 * Math.Pow(interval, 3)) + + (5444 * Math.Pow(interval, 4)) + + (-13860 * Math.Pow(interval, 5)) + + (16731 * Math.Pow(interval, 6)) + + (-7118 * Math.Pow(interval, 7)), 0, 100); + + /// + /// Given an interval between 0 and 1, return the brightness level for + /// the color at that interval along the ramp (0-100). + /// This was determined from using polynomial regression based on the values + /// provided by slynyrd. + /// https://www.slynyrd.com/blog/2018/1/10/pixelblog-1-color-palettes + /// + /// + /// Color position along the ramp as a value between 0 + /// and 1. + /// + /// Saturation value between 0 and 100. + public static double SlynyrdBrightnessFunction(double interval) => + Math.Clamp(15 + (110 * interval) + (347 * Math.Pow(interval, 2)) + + (-1453 * Math.Pow(interval, 3)) + + (2416 * Math.Pow(interval, 4)) + + (-1888 * Math.Pow(interval, 5)) + + (554 * Math.Pow(interval, 6)), 0, 100); + + /// + /// Converts HSV color values to RGB + /// + /// 0 - 360 + /// 0 - 100 + /// 0 - 100 + public static SKColor HsvToRgb(int h, int s, int v) { + Span rgb = stackalloc int[3]; + + while (h < 0) { + h += 360; + } + + while (h >= 360) { + h -= 360; + } + + var baseColor = (h + 60) % 360 / 120; + var shift = ((h + 60) % 360) - ((120 * baseColor) + 60); + var secondaryColor = (baseColor + (shift >= 0 ? 1 : -1) + 3) % 3; + + // Setting Hue + rgb[baseColor] = 255; + rgb[secondaryColor] = (int)(Math.Abs(shift) / 60.0f * 255.0f); + + // Setting Saturation + for (var i = 0; i < 3; i++) { + rgb[i] += (int)((255 - rgb[i]) * ((100 - s) / 100.0f)); + } + + // Setting Value + for (var i = 0; i < 3; i++) { + rgb[i] -= (int)(rgb[i] * (100 - v) / 100.0f); + } + + return new SKColor((byte)rgb[0], (byte)rgb[1], (byte)rgb[2]); + } +} diff --git a/Chickensoft.PalettePainter/src/Generate.cs b/Chickensoft.PalettePainter/src/Generate.cs new file mode 100644 index 0000000..717c54a --- /dev/null +++ b/Chickensoft.PalettePainter/src/Generate.cs @@ -0,0 +1,237 @@ +namespace Chickensoft.PalettePainter; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using SkiaSharp; + +[Command("generate", Description = "Generate a palette of colors.")] +public class Generate : ICommand +{ + private const int NUM_RAMPS_DEFAULT = 16; + private const int NUM_COLORS_PER_RAMP_DEFAULT = 9; + private const int NUM_COLORS_TO_TRIM_FOR_DESATURATED_RAMP_DEFAULT = 2; + private const double DESATURATE_DEFAULT = 0.3f; + private const int SCALE_DEFAULT = 1; + private const int HUE_DEFAULT = 0; + private const double HUE_SHIFT_DEFAULT = 0.5f; + private const double SATURATION_DEFAULT = 1f; + private const double BRIGHTNESS_DEFAULT = 1f; + private const int HUE_SPECTRUM_DEFAULT = 360; + + [CommandParameter(0, Name = "output", + Description = "Palette output image file path", IsRequired = true)] + public required string Output { get; init; } + + [CommandOption("num-ramps", 'n', + Description = "Number of color ramps to generate", IsRequired = false)] + public int NumRamps { get; init; } = NUM_RAMPS_DEFAULT; + + [CommandOption("num-colors-per-ramp", 'c', + Description = "Number of colors per ramp", IsRequired = false)] + public int NumColorsPerRamp { get; init; } = NUM_COLORS_PER_RAMP_DEFAULT; + + [CommandOption("num-colors-to-trim-for-desaturated-ramp", 'z', + Description = "Number of colors to remove from the ends of the ramp when " + + "constructing the desaturated color ramp variant.", + IsRequired = false)] + public int NumColorsToTrimForDesaturatedRamp { get; init; } = + NUM_COLORS_TO_TRIM_FOR_DESATURATED_RAMP_DEFAULT; + + [CommandOption("desaturate", 'd', + Description = "Percentage of saturation to keep when creating a " + + "desaturated variant of a color.")] + public double Desaturate { get; init; } = DESATURATE_DEFAULT; + + [CommandOption("scale", 'x', + Description = "Scale factor for the output image (how big a single pixel " + + "should be).", IsRequired = false)] + public int Scale { get; init; } = SCALE_DEFAULT; + + [CommandOption("hue", 'h', + Description = "Starting hue of the middle color in the first ramp.")] + public int Hue { get; init; } = HUE_DEFAULT; + + [CommandOption("saturation", 's', + Description = "Percentage of saturation to keep when creating a color. " + + "Colors follow the built-in artistic saturation function. " + + "This value determines how much of the saturation function " + + "to use. Values greater than 1.0 will over-apply the " + + "result and can potentially result in color data loss.")] + public double Saturation { get; init; } = SATURATION_DEFAULT; + + [CommandOption("brightness", 'b', + Description = "Percentage of brightness to keep when creating a color. " + + "Colors follow the built-in artistic brightness function. " + + "This value determines how much of the brightness function " + + "to use. Values greater than 1.0 will over-apply the " + + "result and can potentially result in color data loss.")] + public double Brightness { get; init; } = BRIGHTNESS_DEFAULT; + + [CommandOption("hue-shift", 'm', + Description = + "How much of the hue spectrum (as a fraction between 0 and 1) should " + + " be covered by a single color ramp.")] + public double HueShift { get; init; } = HUE_SHIFT_DEFAULT; + + [CommandOption("hue-spectrum", 'u', + Description = "The amount of the hue spectrum to cover in the palette " + + "(0-360).")] + public int HueSpectrum { get; init; } = HUE_SPECTRUM_DEFAULT; + + // Computed + public double TotalChangeInHuePerRamp => 360 * HueShift; + + public double ChangeInHuePerColor => + TotalChangeInHuePerRamp / NumColorsPerRamp; + + public int NumDesaturatedColors => + NumColorsPerRamp - NumColorsToTrimForDesaturatedRamp; + + public ValueTask ExecuteAsync(IConsole console) + { + // a palette is a list of ramps + List> palette = []; + + var cwd = Environment.CurrentDirectory; + var outputFile = Path.Combine(cwd, Output); + + console.Output.WriteLine("Creating a palette..."); + + for (var rampIndex = 0; rampIndex < NumRamps; rampIndex++) + { + var hue = (int)(Hue + ((double)rampIndex / NumRamps * HueSpectrum)); + + var colors = GetRamp( + NumColorsPerRamp, + ChangeInHuePerColor, + hue, + Saturation, + Brightness, + 0d, + 1d + ); + + // compute the start progress and end progress such that the desaturated + // color ramp trims the lightest and darkest colors from the ramp since + // those colors would be nearly equivalent to their counterparts in + // the saturated ramp. + var desaturatedStartP = + NumColorsToTrimForDesaturatedRamp / 2d / NumColorsPerRamp; + var desaturatedEndP = 1 - desaturatedStartP; + + var desaturatedColors = GetRamp( + NumDesaturatedColors, + ChangeInHuePerColor, + hue, + Desaturate, + Brightness, + desaturatedStartP, + desaturatedEndP + ).Reverse(); + + var ramp = colors.Concat(desaturatedColors).ToList(); + + palette.Add(ramp); + } + + var image = Render(palette); + SaveImage(outputFile, image); + + console.Output.WriteLine($"Palette saved: {outputFile}"); + + return new(); + } + + public static List GetRamp( + int numColors, + double changeInHuePerColor, + int hue, + double saturation, + double brightness, + double startP, /* start percentage */ + double endP /* end percentage */ + ) + { + var colors = new SKColor[numColors]; + + var middleIndex = numColors / 2; + + var changeInHuePerColorRemapped = changeInHuePerColor * (endP - startP); + + // create colors (from darkest to lightest) + for (var colorIndex = 0; colorIndex < numColors; colorIndex++) + { + var p = (double)colorIndex / numColors; + var remappedP = startP + (p * (endP - startP)); + var distanceFromMiddle = middleIndex - colorIndex; + + var h = hue - (distanceFromMiddle * changeInHuePerColorRemapped); + var s = Colors.SlynyrdSaturationFunction(remappedP) * saturation; + var b = Colors.SlynyrdBrightnessFunction(remappedP) * brightness; + + colors[colorIndex] = Colors.HsvToRgb((int)h, (int)s, (int)b); + } + + return [.. colors]; + } + + public SKImage Render(List> palette) + { + var surface = SKSurface.Create( + new SKImageInfo( + (Scale * NumColorsPerRamp) + + (Scale * (NumColorsPerRamp - NumColorsToTrimForDesaturatedRamp)), + Scale * NumRamps + ) + ); + + var canvas = surface.Canvas; + + canvas.Clear(SKColors.Black); + + using var paint = new SKPaint(); + + paint.IsAntialias = false; + paint.Style = SKPaintStyle.Fill; + + // draw swatches from the palette + for (var rampIndex = 0; rampIndex < NumRamps; rampIndex++) + { + var ramp = palette[rampIndex]; + + for (var colorIndex = 0; colorIndex < ramp.Count; colorIndex++) + { + var color = ramp[colorIndex]; + + paint.Color = new SKColor(color.Red, color.Green, color.Blue); + + var x = colorIndex; + var y = rampIndex; + + canvas.DrawRect( + x * Scale, y * Scale, Scale, Scale, paint + ); + } + } + + return surface.Snapshot(); + } + + public void SaveImage(string fullyQualifiedPath, SKImage image) + { + using var data = image.Encode(SKEncodedImageFormat.Png, 100); + + using var stream = File.OpenWrite(fullyQualifiedPath); + + data.SaveTo(stream); + + stream.Flush(); + stream.Close(); + } +} diff --git a/Chickensoft.PalettePainter/src/Main.cs b/Chickensoft.PalettePainter/src/Main.cs new file mode 100644 index 0000000..ba99c10 --- /dev/null +++ b/Chickensoft.PalettePainter/src/Main.cs @@ -0,0 +1,26 @@ +namespace Chickensoft.PalettePainter; + +using System.Reflection; +using System.Threading.Tasks; +using CliFx; +public static class PalettePainter { + public static async Task Main(string[] args) { + var version = Assembly + .GetEntryAssembly()! + .GetCustomAttribute()! + .InformationalVersion; + + return await new CliApplicationBuilder() + .SetExecutableName("palettepainter") + .SetTitle("Palette Painter") + .SetVersion(version) + .SetDescription( + """ + Create a palette of colors with the specified number of ramps. You may + customize hue shifts and saturation levels to create a unique palette. + """ + ) + .AddCommandsFromThisAssembly() + .Build().RunAsync(args); + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ad0e620 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +# MIT License + +Copyright (c) 2023 Your Name + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..14fea85 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# 🎨 PalettePainter + +[![Chickensoft Badge][chickensoft-badge]][chickensoft-website] [![Discord][discord-badge]][discord] + +Command-line, general-purpose palette generator for use with pixel art, textures, or art software. Palettes are constructed based on the principles described in [Slynyrd][slynyrd]'s seminal blog on [pixel art color palettes][palettes]. + +--- + +

+Chickensoft.PalettePainter +

+ +## 📦 Installation + +This is a .NET Framework 8 tool written in C#. Once you have .NET installed, you can install this tool globally: + +```sh +dotnet tool install -g Chickensoft.PalettePainter + +# Run the tool +palettepainter generate --help +``` + +## 🖼️ Quick Start + +```sh +palettepainter generate palette.png --scale 12 +``` + +![default palette](doc_assets/default.png) + +> [!NOTE] +The `--scale` or `-x` parameter controls how large a pixel is. These images were generated with `--scale 12` so that each palette swatch is 12x12 pixels. + +## Winter Colors + +```sh +palettepainter generate palette.png --hue 180 --saturation 1 --brightness 1 --num-ramps 8 --hue-shift 0.5 --hue-spectrum 100 --desaturate 0.3 --scale 12 +``` + +![default palette](doc_assets/winter.png) + +## Wooden Colors + +```sh +palettepainter generate palette.png --hue 20 --saturation 1 --brightness 1 --num-ramps 4 --hue-shift 0.1 --hue-spectrum 15 --desaturate 0.6 --scale 12 +``` + +![default palette](doc_assets/wooden.png) + +## Leafy Greens + +```sh +palettepainter generate palette.png --hue 110 --saturation 1 --brightness 1 --num-ramps 6 --hue-shift 0.5 --hue-spectrum 60 --desaturate 0.6 --scale 12 +``` + +![default palette](doc_assets/leafy_greens.png) + +## Tiny Full Spectrum + +```sh +palettepainter generate palette.png --hue 110 --saturation 1 --brightness 1 --num-ramps 4 --num-colors-per-ramp 12 -z 8 --hue-shift 0.5 --hue-spectrum 360 --desaturate 0.3 --scale 12 +``` + +![default palette](doc_assets/tiny_full_spectrum.png) + +...etc! + +## 🖥️ Usage Details + +PalettePainter is a command-line tool that exposes a number of variables to help you generate the type of general-purpose palette you're looking for. Each of these parameters is described in the help text `palettepainter generate --help`: + +```plaintext +Palette Painter 1.0.0 + Create a palette of colors with the specified number of ramps. You may +customize hue shifts and saturation levels to create a unique palette. + +USAGE + palettepainter generate [options] + +DESCRIPTION + Generate a palette of colors. + +PARAMETERS +* output Palette output image file path + +OPTIONS + -n|--num-ramps Number of color ramps to generate Default: "16". + -c|--num-colors-per-ramp Number of colors per ramp Default: "9". + -z|--num-colors-to-trim-for-desaturated-ramp Number of colors to remove from the ends of the ramp when constructing the desaturated color ramp variant. Default: "2". + -d|--desaturate Percentage of saturation to keep when creating a desaturated variant of a color. Default: "0.30000001192092896". + -x|--scale Scale factor for the output image (how big a single pixel should be). Default: "1". + -h|--hue Starting hue of the middle color in the first ramp. Default: "0". + -s|--saturation Percentage of saturation to keep when creating a color. Colors follow the built-in artistic saturation function. This value determines how much of the saturation function to use. Values greater than 1.0 will over-apply the result and can potentially result in color data loss. Default: "1". + -b|--brightness Percentage of brightness to keep when creating a color. Colors follow the built-in artistic brightness function. This value determines how much of the brightness function to use. Values greater than 1.0 will over-apply the result and can potentially result in color data loss. Default: "1". + -m|--hue-shift How much of the hue spectrum (as a fraction between 0 and 1) should be covered by a single color ramp. Default: "0.5". + -u|--hue-spectrum The amount of the hue spectrum to cover in the palette (0-360). Default: "360". + -h|--help Shows help text. +``` + +--- + +🐣 Package generated from a 🐤 Chickensoft Template — + +[chickensoft-badge]: https://raw.githubusercontent.com/chickensoft-games/chickensoft_site/main/static/img/badges/chickensoft_badge.svg +[chickensoft-website]: https://chickensoft.games +[discord-badge]: https://raw.githubusercontent.com/chickensoft-games/chickensoft_site/main/static/img/badges/discord_badge.svg +[discord]: https://discord.gg/gSjaPgMmYW + +[slynyrd]: https://www.slynyrd.com/pixelblog-catalogue +[palettes]: https://www.slynyrd.com/blog/2018/1/10/pixelblog-1-color-palettes diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..baf9de1 --- /dev/null +++ b/cspell.json @@ -0,0 +1,76 @@ +{ + "files": [ + "**/*.*" + ], + "ignorePaths": [ + "**/*.tscn", + "**/*.import", + "**/badges/**/*.*", + "**/coverage/**/*.*", + "**/.godot/**/*.*", + "**/obj/**/*.*", + "**/bin/**/*.*", + "**/nupkg/**/*.*" + ], + "words": [ + "assemblyfilters", + "automerge", + "branchcoverage", + "brandedoutcast", + "buildtransitive", + "camelcase", + "chickenpackage", + "chickensoft", + "Chickensoft", + "classfilters", + "contentfiles", + "CYGWIN", + "desaturate", + "desaturated", + "devbuild", + "endregion", + "Finalizer", + "Finalizers", + "globaltool", + "godotengine", + "godotpackage", + "issuecomment", + "justalemon", + "lcov", + "lihop", + "linecoverage", + "methodcoverage", + "missingall", + "msbuild", + "MSYS", + "nameof", + "Nerdbank", + "netstandard", + "NOLOGO", + "nupkg", + "Omnisharp", + "opencover", + "OPTOUT", + "palettepainter", + "paramref", + "pascalcase", + "Postinitialize", + "Predelete", + "renovatebot", + "reportgenerator", + "reporttypes", + "Shouldly", + "Skia", + "slynyrd", + "subfolders", + "targetargs", + "targetdir", + "tscn", + "typeof", + "typeparam", + "typeparamref", + "ulong", + "Unparented", + "Xunit" + ] +} diff --git a/doc_assets/default.png b/doc_assets/default.png new file mode 100644 index 0000000..aa72c98 --- /dev/null +++ b/doc_assets/default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28e1f05363fcfc231c0afd7c26ffbc5ab44c59df23045f87f55f46762c4d5f2d +size 1507 diff --git a/doc_assets/leafy_greens.png b/doc_assets/leafy_greens.png new file mode 100644 index 0000000..603b2f2 --- /dev/null +++ b/doc_assets/leafy_greens.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15906ad4d0179e14f34e4fd3944b2c1941d9d5f8fec6fb25171b1121f0e96afa +size 1648 diff --git a/doc_assets/tiny_full_spectrum.png b/doc_assets/tiny_full_spectrum.png new file mode 100644 index 0000000..ec9cb5f --- /dev/null +++ b/doc_assets/tiny_full_spectrum.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e0a551063f417347371df13e5616106df3a432366c15c435117b7d3ae818d85 +size 693 diff --git a/doc_assets/winter.png b/doc_assets/winter.png new file mode 100644 index 0000000..c39aa23 --- /dev/null +++ b/doc_assets/winter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cee71c85462c34f34e08521960d0cf5bfb67e30aa51a482cb582f21a312dac3 +size 1648 diff --git a/doc_assets/wooden.png b/doc_assets/wooden.png new file mode 100644 index 0000000..b72442a --- /dev/null +++ b/doc_assets/wooden.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:672dd9f1a905651ff0b36e42a8491e653040fd3046e3b87ec706af393a64580a +size 1648 diff --git a/global.json b/global.json new file mode 100644 index 0000000..b2dc48a --- /dev/null +++ b/global.json @@ -0,0 +1,9 @@ +{ + "sdk": { + "version": "8.0.204", + "rollForward": "latestMinor" + }, + "msbuild-sdks": { + "Godot.NET.Sdk": "4.2.2" + } +} diff --git a/omnisharp.json b/omnisharp.json new file mode 100644 index 0000000..f7e0f3c --- /dev/null +++ b/omnisharp.json @@ -0,0 +1,5 @@ +{ + "FormattingOptions": { + "enableEditorConfigSupport": true + } +} diff --git a/output.png b/output.png new file mode 100644 index 0000000..e866aa7 --- /dev/null +++ b/output.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed1308d61c217dd8a82ecf129cecd1f09d347d067c2b1c420110f506c4d36027 +size 894 diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..6e2564c --- /dev/null +++ b/renovate.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base", + ":semanticCommits" + ], + "prHourlyLimit": 2, + "versioning": "loose", + "packageRules": [ + { + "matchPackagePatterns": [ + "*" + ], + "groupName": "all dependencies", + "groupSlug": "all-deps", + "automerge": true + }, + { + "matchPackagePrefixes": [ + "dotnet-sdk" + ], + "allowedVersions": "!/preview/" + }, + { + "matchPackagePrefixes": [ + "GodotSharp", + "Godot.NET.Sdk" + ], + "allowedVersions": "/^(\\d+\\.\\d+\\.\\d+)(-(beta|rc)\\.(\\d+)(\\.\\d+)*)?$/" + }, + { + "matchPackagePrefixes": [ + "Chickensoft" + ], + "allowedVersions": "/^(\\d+\\.\\d+\\.\\d+)(-godot(\\d+\\.)+\\d+(-.*)?)?$/" + } + ] +} \ No newline at end of file