From 9ee24a066b4f6a02891c8118db007073f646b612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Thu, 13 Aug 2026 04:07:24 +0200 Subject: [PATCH] Normalize package --- .gitattributes | 18 ++- .github/workflows/CI.yml | 15 +- .gitignore | 10 +- .php-cs-fixer.dist.php | 23 ++- CHANGELOG.md | 6 +- LICENSE | 2 +- README.md | 224 ++++++++++----------------- composer.json | 16 +- docs/getting-started.md | 33 ++++ docs/guessing.md | 35 +++++ docs/index.md | 34 ++++ docs/installation.md | 26 ++++ docs/linting.md | 41 +++++ docs/scales/fibonacci.md | 29 ++++ docs/scales/index.md | 27 ++++ docs/scales/linear.md | 32 ++++ docs/scales/modular.md | 55 +++++++ docs/scales/multi-strand.md | 29 ++++ phpstan.dist.neon | 5 - phpstan.neon.dist | 7 + phpunit.dist.xml => phpunit.xml.dist | 23 +-- src/AbstractScale.php | 2 +- src/Exception/ScaleException.php | 6 +- src/FibonacciScale.php | 2 +- src/LinearScale.php | 2 +- src/ModularScale.php | 2 +- src/MultiStrandScale.php | 8 +- src/Ratio.php | 2 +- src/Scale.php | 2 +- src/ScaleGuesser.php | 10 +- src/ScaleInterface.php | 2 +- src/ScaleLinter.php | 8 +- tests/FibonacciScaleTest.php | 2 +- tests/LinearScaleTest.php | 2 +- tests/ModularScaleTest.php | 2 +- tests/MultiStrandScaleTest.php | 2 +- tests/RatioTest.php | 2 +- tests/ScaleFacadeTest.php | 2 +- tests/ScaleGuesserTest.php | 2 +- tests/ScaleLinterTest.php | 2 +- 40 files changed, 508 insertions(+), 244 deletions(-) create mode 100644 docs/getting-started.md create mode 100644 docs/guessing.md create mode 100644 docs/index.md create mode 100644 docs/installation.md create mode 100644 docs/linting.md create mode 100644 docs/scales/fibonacci.md create mode 100644 docs/scales/index.md create mode 100644 docs/scales/linear.md create mode 100644 docs/scales/modular.md create mode 100644 docs/scales/multi-strand.md delete mode 100644 phpstan.dist.neon create mode 100644 phpstan.neon.dist rename phpunit.dist.xml => phpunit.xml.dist (54%) diff --git a/.gitattributes b/.gitattributes index 39bc8ae..d372ca0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,13 @@ -/.git* export-ignore -/.php-cs-fixer.dist.php export-ignore -/phpstan.dist.neon export-ignore -/phpunit.dist.xml export-ignore -/tests/ export-ignore +* text=auto eol=lf + +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.php-cs-fixer.dist.php export-ignore +/docs export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore + +/docs/** linguist-documentation +/tests/Fixtures/** -linguist-detectable diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index e36ea4f..21c8d6d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ "*" ] + branches: ["*"] pull_request: - branches: [ "*" ] + branches: ["*"] workflow_dispatch: permissions: @@ -15,26 +15,17 @@ concurrency: cancel-in-progress: true jobs: - cs: uses: altophp/.github/.github/workflows/CS.yml@main - # with: - # php-version: '8.5' - # composer-validate: true - # php-cs-fixer-args: '--diff --dry-run' sa: uses: altophp/.github/.github/workflows/SA.yml@main - # with: - # php-version: '8.5' - # phpstan-args: 'analyse --no-progress --memory-limit=-1' tests: strategy: fail-fast: false matrix: - php: ['8.4', '8.5'] + php: ["8.4", "8.5"] uses: altophp/.github/.github/workflows/tests.yml@main with: php-version: ${{ matrix.php }} - # phpunit-args: '--colors=never' diff --git a/.gitignore b/.gitignore index d8135fb..8346dbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ -/.phpunit.cache/ /vendor/ -/.php-cs-fixer.cache /composer.lock -/phpstan.neon -/phpunit.xml +/.phpunit.cache/ +/.phpunit.result.cache +/.php-cs-fixer.cache +/coverage/ +/coverage.xml +/clover.xml diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 39c7b23..1bcabeb 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,29 +1,28 @@ in(__DIR__) - ->exclude([ - 'var/', - ]) -; +$finder = PhpCsFixer\Finder::create() + ->in([__DIR__.'/src', __DIR__.'/tests']) + ->exclude('Fixtures'); return (new PhpCsFixer\Config()) - ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) - ->setFinder($finder) ->setRiskyAllowed(true) ->setRules([ - '@PER-CS' => true, - '@Symfony' => true, + '@PER-CS2.0' => true, 'declare_strict_types' => true, 'header_comment' => ['header' => $licence], + 'no_unused_imports' => true, + 'ordered_imports' => ['imports_order' => ['class', 'function', 'const']], + 'phpdoc_line_span' => ['const' => 'multi', 'property' => 'multi', 'method' => 'multi'], ]) -; + ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md index f10b7a3..fd837d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## [Unreleased] + +- Add documentation. + ## [1.0.0] - 2026-01-17 -* Initial release +- Initial release. diff --git a/LICENSE b/LICENSE index 9310ffc..dc9b29f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Simon André +Copyright (c) 2026-present Simon André Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4c07f5f..96d5f50 100644 --- a/README.md +++ b/README.md @@ -1,204 +1,136 @@ -# Alto \ Scale +# ALTO Scale -A strict, mathematical engine for **Modular Scales**. -It handles the arithmetic of progressions, designed for generative design, typography engines, and rhythmic computation. +Strict mathematical scales for typography, spacing, grids, and rhythmic design systems. ---- +  ![PHP Version](https://img.shields.io/badge/PHP-8.4%2B-00B7FF?logoColor=00B7FF&labelColor=050608) +  ![CI](https://img.shields.io/github/actions/workflow/status/altophp/scale/CI.yml?branch=main&label=Tests&labelColor=050608&color=00B7FF) +  [![Packagist](https://img.shields.io/packagist/v/alto/scale?label=Packagist&labelColor=050608&color=00B7FF)](https://packagist.org/packages/alto/scale) +  ![License](https://img.shields.io/github/license/altophp/scale?label=License&labelColor=050608&color=00B7FF) +  [![GitHub Sponsors](https://img.shields.io/github/sponsors/smnandre?logo=githubsponsors&logoColor=00B7FF&label=%20Sponsor&labelColor=050608&color=00B7FF)](https://github.com/sponsors/smnandre) -  [![PHP Version](https://img.shields.io/badge/PHP-8.4+-ffefdf?logoColor=white&labelColor=000)](https://github.com/altophp/scale) -  [![Packagist Version](https://img.shields.io/packagist/v/alto/scale?label=Stable&logoColor=white&logoSize=auto&labelColor=000&color=ffefdf)](https://packagist.org/packages/alto/scale) -  [![CI](https://img.shields.io/github/actions/workflow/status/altophp/scale/CI.yml?branch=main&label=Tests&logoColor=white&logoSize=auto&labelColor=000&color=ffefdf)](https://github.com/altophp/scale/actions) -  [![PHP Version](https://img.shields.io/badge/PHPUnit-100%25-ffefdf?logoColor=white&labelColor=000)](https://github.com/altophp/scale) -  [![PHP Version](https://img.shields.io/badge/PHPStan-LVL%2010-ffefdf?logoColor=white&labelColor=000)](https://github.com/altophp/scale) -  [![License](https://img.shields.io/github/license/altophp/scale?label=License&logoColor=white&logoSize=auto&labelColor=000&color=ffefdf)](./LICENSE) +ALTO Scale turns a mathematical progression into predictable design values. Build modular, +linear, Fibonacci, or multi-strand scales through one small API, then generate ranges, snap +arbitrary values, infer an existing scale, or lint a collection. +```php +use Alto\Scale\Scale; + +$type = Scale::majorThird(16); + +$type->range(-1, 2); +// [-1 => 12.8, 0 => 16.0, 1 => 20.0, 2 => 25.0] +``` + +The package has no runtime dependencies. Every scale implements the same typed interface and the +codebase is checked at PHPStan's maximum level. ## Installation +Install ALTO Scale with Composer: + ```bash composer require alto/scale ``` -## Core Concepts +ALTO Scale requires PHP 8.4 or later. -### 1. The Scale Facade +## Quick Start -Use the `Scale` facade for fluent instantiation of any scale type. +Create a scale through the `Scale` facade and request the values needed by your design system: ```php use Alto\Scale\Scale; -use Alto\Scale\Ratio; - -// Modular (Geometric): Base 16px, Ratio 1.25 (Major Third) -$type = Scale::majorThird(16.0); -// Linear (Arithmetic): Base 0, Increment 8 -$grid = Scale::linear(0, 8); +$spacing = Scale::linear(base: 0, increment: 8); -// Fibonacci: Using Binet's formula -$fib = Scale::fibonacci(); - -// Multi-Strand: Interleaved scales (e.g., 16px and 12px strands) -$strands = Scale::strands([16, 12], Ratio::PerfectFifth); +echo $spacing->get(3); // 24 +echo $spacing->snap(19); // 16 ``` -### 2. Computing Values +All scales expose `get()`, `stepOf()`, `snap()`, and `range()` and can be iterated over steps zero +through ten. -All scales implement the `ScaleInterface` and are `IteratorAggregate`. +## Scale Types -```php -echo $type->get(0); // 16.0 -echo $type->get(1); // 20.0 -echo $type->get(-1); // 12.8 +| Scale | Progression | Typical use | +| --- | --- | --- | +| Modular | Multiply by one ratio | Type sizes and proportional spacing | +| Linear | Add one increment | Baseline grids and fixed spacing | +| Fibonacci | Follow Fibonacci numbers | Integer rhythms and counts | +| Multi-strand | Interleave modular scales | Multiple coordinated bases | -// ModularScale is callable -echo $type(2); // 25.0 -``` - -#### Snapping (Normalization) - -Find the nearest mathematical step for any arbitrary value. +Named constructors provide common ratios: ```php -// Returns 20.0 (The nearest step on a Major Third scale starting at 16) -$value = $type->snap(19.8); +$type = Scale::majorThird(16); +$display = Scale::perfectFifth(48); +$golden = Scale::golden(1); ``` -#### Iteration & Ranges +Custom ratios are available through `Scale::modular()`. See the +[scale guide](docs/scales/index.md) for every progression and its constraints. -```php -// Scales are iterable (defaults to steps 0-10) -foreach ($type as $step => $value) { - // ... -} - -// Custom ranges -$values = $type->range(-2, 2); -// Returns: [-2 => 10.24, -1 => 12.8, 0 => 16.0, 1 => 20.0, 2 => 25.0] -``` +## Guessing -### 3. The Guesser (Reverse Engineering) - -Analyze messy datasets to find their underlying mathematical scale. +Infer a modular scale from existing positive values and align the originals to it: ```php -use Alto\Scale\Scale; use Alto\Scale\ScaleGuesser; -$messyValues = [15.9, 20.1, 24.8, 31.5]; - -// Via the facade -$scale = Scale::guess($messyValues); - -// Or directly with custom tolerance +$values = [15.9, 20.1, 24.8, 31.5]; $guesser = new ScaleGuesser(tolerance: 0.05); -$scale = $guesser->guess($messyValues); -// Align values to the guessed (or provided) scale -$aligned = $guesser->align($messyValues); +$scale = $guesser->guess($values); +$aligned = $guesser->align($values, $scale); ``` -### 4. The Linter (Audit & Fix) +Read [Guessing a scale](docs/guessing.md) for the estimation rules and limitations. -Audit datasets for scale compliance and automatically harmonize them. +## Linting + +Audit values against a known scale and normalize deviations: ```php -use Alto\Scale\ScaleLinter; use Alto\Scale\Scale; +use Alto\Scale\ScaleLinter; $linter = new ScaleLinter(Scale::linear(0, 8)); -// Get a detailed compliance report $report = $linter->lint([8, 15, 24]); -// Each entry: ['original', 'suggested', 'step', 'deviation', 'isValid'] - -// Returns [8.0, 16.0, 24.0] -$fixed = $linter->fix([8, 15, 24]); +$fixed = $linter->fix([8, 15, 24]); // [8.0, 16.0, 24.0] ``` -## API Reference - -### `Alto\Scale\Scale` (Facade) - -| Method | Returns | -|----------------------------------------------|--------------------| -| `modular(float $base, float\|Ratio $ratio)` | `ModularScale` | -| `linear(float $base, float $increment)` | `LinearScale` | -| `fibonacci(float $multiplier)` | `FibonacciScale` | -| `strands(array $bases, float\|Ratio $ratio)` | `MultiStrandScale` | -| `guess(array $values)` | `ModularScale` | -| `majorThird(float $base)` | `ModularScale` | -| `perfectFifth(float $base)` | `ModularScale` | -| `golden(float $base)` | `ModularScale` | - -### `Alto\Scale\ScaleInterface` - -All scale classes (`ModularScale`, `LinearScale`, `FibonacciScale`, `MultiStrandScale`) implement: - -| Method | Description | -|------------------------------------|-----------------------------------------| -| `get(int $step): float` | Get the value at a specific step | -| `stepOf(float $value): int` | Find the closest step index for a value | -| `snap(float $value): float` | Snap a value to the nearest step | -| `range(int $min, int $max): array` | Generate values between steps | - -### `Alto\Scale\ModularScale` +Read [Linting values](docs/linting.md) for the report format and inferred-scale behavior. The +[complete documentation](docs/index.md) also covers installation, the shared API, and each scale +type. -Geometric progression: `value = base * (ratio ^ step)` +## Contributing -| Method | Description | -|---------------------------------------------------------|----------------------------------------------| -| `areHarmonic(float $a, float $b, float $epsilon): bool` | Check if two values are harmonically related | -| `withBase(float $base): self` | Create a new scale with a different base | -| `withRatio(float\|Ratio $ratio): self` | Create a new scale with a different ratio | -| `shift(int $steps): self` | Create a new scale shifted by N steps | +Contributions of all kinds are welcome. Visit the +[project on GitHub](https://github.com/altophp/scale) to +[report a bug](https://github.com/altophp/scale/issues/new), +[suggest a feature](https://github.com/altophp/scale/issues/new), or +[open a pull request](https://github.com/altophp/scale/pulls). -### `Alto\Scale\LinearScale` +Before submitting code, run: -Arithmetic progression: `value = base + (step * increment)` - -### `Alto\Scale\FibonacciScale` - -Fibonacci sequence using Binet's formula, with optional multiplier. - -### `Alto\Scale\MultiStrandScale` - -Interleaved modular scales sharing a common ratio, useful for multi-strand typographic scales. - -### `Alto\Scale\ScaleGuesser` - -| Method | Description | -|-------------------------------------------------------|-----------------------------------| -| `guess(array $values): ModularScale` | Infer a modular scale from values | -| `align(array $values, ?ScaleInterface $scale): array` | Snap all values to a scale | - -### `Alto\Scale\ScaleLinter` +```bash +# Runs PHP CS Fixer, PHPStan, and PHPUnit +composer qa +``` -| Method | Description | -|------------------------------|-------------------------------| -| `lint(array $values): array` | Audit values against a scale | -| `fix(array $values): array` | Harmonize values to the scale | +Changes to public behavior should include tests and documentation. -### `Alto\Scale\Ratio` (Enum) +## Support -Standard musical/typographic ratios: +ALTO Scale is open source. You can support its continued development through +[GitHub Sponsors](https://github.com/sponsors/smnandre). -| Ratio | Value | -|-------------------|-------| -| `MinorSecond` | 1.067 | -| `MajorSecond` | 1.125 | -| `MinorThird` | 1.200 | -| `MajorThird` | 1.250 | -| `PerfectFourth` | 1.333 | -| `AugmentedFourth` | 1.414 | -| `PerfectFifth` | 1.500 | -| `MinorSixth` | 1.600 | -| `GoldenRatio` | 1.618 | -| `MajorSixth` | 1.667 | -| `MinorSeventh` | 1.778 | -| `MajorSeventh` | 1.875 | -| `Octave` | 2.000 | +Sharing this package with others or +[starring it on GitHub](https://github.com/altophp/scale) is also much +appreciated. ## License -This project is licensed under the [MIT License](./LICENSE). +ALTO Scale is released by [ALTO PHP](https://altophp.com) under the +[MIT License](LICENSE). diff --git a/composer.json b/composer.json index 0679fb0..267985f 100644 --- a/composer.json +++ b/composer.json @@ -21,10 +21,11 @@ "email": "smn.andre@gmail.com" } ], - "homepage": "https://github.com/altophp/scale", + "homepage": "https://altophp.com/scale", "support": { "issues": "https://github.com/altophp/scale/issues", - "docs": "https://github.com/altophp/scale#readme" + "source": "https://github.com/altophp/scale", + "docs": "https://altophp.com/scale" }, "funding": [ { @@ -36,11 +37,10 @@ "php": "^8.4" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.92.4", + "friendsofphp/php-cs-fixer": "^3.94", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^12.5" }, - "minimum-stability": "stable", "autoload": { "psr-4": { "Alto\\Scale\\": "src/" @@ -52,19 +52,17 @@ } }, "config": { - "allow-plugins": { - "php-http/discovery": true - }, "sort-packages": true }, "scripts": { - "cs": "vendor/bin/php-cs-fixer fix", + "cs": "vendor/bin/php-cs-fixer fix --dry-run --diff --sequential", + "cs:fix": "vendor/bin/php-cs-fixer fix --sequential", "qa": [ "@cs", "@sa", "@test" ], - "sa": "vendor/bin/phpstan analyse", + "sa": "vendor/bin/phpstan analyse --memory-limit=-1", "test": "vendor/bin/phpunit" } } diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..5b6555d --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,33 @@ +# Getting started + +Create a scale through the `Scale` facade, then ask it for the values required +by the current design system. + +```php +use Alto\Scale\Scale; + +$spacing = Scale::linear(base: 0, increment: 8); + +echo $spacing->get(1); // 8 +echo $spacing->get(3); // 24 +``` + +All scales share four operations: + +```php +$spacing->get(4); // 32.0: value at step 4 +$spacing->stepOf(30); // 4: nearest step to 30 +$spacing->snap(30); // 32.0: nearest scale value +$spacing->range(1, 4); // [1 => 8.0, 2 => 16.0, 3 => 24.0, 4 => 32.0] +``` + +Scales are also iterable. Iteration yields steps 0 through 10: + +```php +foreach ($spacing as $step => $value) { + printf("%d: %g\n", $step, $value); +} +``` + +Choose a progression from [All scales](scales/index.md). If values already +exist, use [Guessing](guessing.md) or [Linting](linting.md). diff --git a/docs/guessing.md b/docs/guessing.md new file mode 100644 index 0000000..e0b71d5 --- /dev/null +++ b/docs/guessing.md @@ -0,0 +1,35 @@ +# Guessing a scale + +`ScaleGuesser` estimates a modular scale from at least two positive values. +It averages consecutive ratios, chooses a nearby named `Ratio` when one falls +within tolerance, and estimates a shared base. + +```php +use Alto\Scale\ScaleGuesser; + +$guesser = new ScaleGuesser(tolerance: 0.05); +$scale = $guesser->guess([15.9, 20.1, 24.8, 31.5]); + +echo $scale->base; +echo $scale->multiplier; +``` + +Use the facade for the default tolerance: + +```php +use Alto\Scale\Scale; + +$scale = Scale::guess([16, 20, 25, 31.25]); +``` + +## Align the original values + +```php +$aligned = $guesser->align([15.9, 20.1, 24.8, 31.5], $scale); +``` + +`align()` snaps positive values and preserves zero or negative values. Omit +the second argument to infer the target scale from the same dataset. + +Guessing is an estimate, not statistical model selection. Check the returned +base and ratio before making it part of a design-system contract. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..042719f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,34 @@ +# ALTO Scale + +ALTO Scale computes mathematical progressions for typography, spacing, grids, +and other design values. Every scale can return a step, find the nearest step, +snap an arbitrary value, and generate a range. + +```php +use Alto\Scale\Scale; + +$type = Scale::majorThird(16); + +$type->get(-1); // 12.8 +$type->get(0); // 16.0 +$type->get(1); // 20.0 +Scale::majorThird(16)->snap(19.8); // 20.0 +``` + +## Introduction + +- [Installation](installation.md): install the package and verify the runtime. +- [Getting started](getting-started.md): create and use a first scale. + +## Scales + +- [All scales](scales/index.md): choose a progression for the values you need. +- [Modular](scales/modular.md): generate a geometric progression from a base and ratio. +- [Linear](scales/linear.md): generate values separated by a constant increment. +- [Fibonacci](scales/fibonacci.md): generate a scaled Fibonacci sequence. +- [Multi-strand](scales/multi-strand.md): interleave several modular progressions. + +## Analysis + +- [Guessing](guessing.md): infer a modular scale from existing positive values. +- [Linting](linting.md): audit values and align them to a scale. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..e3d2703 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,26 @@ +# Installation + +ALTO Scale requires PHP 8.4 or later and has no runtime dependencies. + +```bash +composer require alto/scale +``` + +## Verify the installation + +```php +get(3); +``` + +The script prints `24`. + +Invalid bases, ratios, increments, ranges, or datasets raise +`Alto\Scale\Exception\ScaleException`. diff --git a/docs/linting.md b/docs/linting.md new file mode 100644 index 0000000..8d3c6f6 --- /dev/null +++ b/docs/linting.md @@ -0,0 +1,41 @@ +# Linting values + +`ScaleLinter` compares positive values with a supplied scale and reports the +nearest step, suggested value, absolute deviation, and validity. + +```php +use Alto\Scale\Scale; +use Alto\Scale\ScaleLinter; + +$linter = new ScaleLinter( + scale: Scale::linear(0, 8), + tolerance: 0.5, +); + +$report = $linter->lint([8.0, 15.0, 24.0]); +``` + +Each report entry contains: + +```php +[ + 'original' => 15.0, + 'suggested' => 16.0, + 'step' => 2, + 'deviation' => 1.0, + 'isValid' => false, +]; +``` + +Non-positive values are omitted from the report. + +## Fix a collection + +```php +$fixed = $linter->fix([8.0, 15.0, 24.0]); +// [8.0, 16.0, 24.0] +``` + +`fix()` preserves non-positive values and snaps every positive value. If no +scale is supplied to the constructor, the linter first uses `ScaleGuesser` +with the same tolerance. diff --git a/docs/scales/fibonacci.md b/docs/scales/fibonacci.md new file mode 100644 index 0000000..ec6380b --- /dev/null +++ b/docs/scales/fibonacci.md @@ -0,0 +1,29 @@ +# Fibonacci scales + +A Fibonacci scale generates the familiar integer sequence and applies an +optional positive multiplier. + +```php +use Alto\Scale\Scale; + +$sequence = Scale::fibonacci(); + +$values = Scale::fibonacci()->range(0, 8); +// [0.0, 1.0, 1.0, 2.0, 3.0, 5.0, 8.0, 13.0, 21.0] +``` + +Use a multiplier when the sequence represents a larger unit: + +```php +$rhythm = Scale::fibonacci(multiplier: 4); + +$rhythm->get(5); // 20.0 +$rhythm->stepOf(19); // 5 +Scale::fibonacci(multiplier: 4)->snap(19); // 20.0 +``` + +Negative steps follow the negafibonacci sequence. Lookup of a non-positive +value returns step zero. + +The implementation uses Binet's formula and rounds to Fibonacci integers +before applying the multiplier. The multiplier must be positive. diff --git a/docs/scales/index.md b/docs/scales/index.md new file mode 100644 index 0000000..ee7791f --- /dev/null +++ b/docs/scales/index.md @@ -0,0 +1,27 @@ +# Scale types + +Choose the progression according to the relationship between consecutive +values. + +| Scale | Relationship | Typical use | +| --- | --- | --- | +| [Modular](modular.md) | Multiply by one ratio | Type sizes, proportional spacing | +| [Linear](linear.md) | Add one increment | Baseline grids, fixed spacing | +| [Fibonacci](fibonacci.md) | Follow Fibonacci numbers | Integer rhythms and counts | +| [Multi-strand](multi-strand.md) | Interleave several modular scales | Multiple coordinated bases | + +Every type implements `ScaleInterface`, so application code can accept any +scale while using `get()`, `stepOf()`, `snap()`, and `range()`. + +```php +use Alto\Scale\ScaleInterface; + +function tokens(ScaleInterface $scale): array +{ + return $scale->range(-2, 5); +} +``` + +The shared contract does not imply identical domains. Modular and multi-strand +scales require positive values for logarithmic lookup. Linear scales can cross +zero. Fibonacci lookup maps non-positive values to step zero. diff --git a/docs/scales/linear.md b/docs/scales/linear.md new file mode 100644 index 0000000..4a28ed2 --- /dev/null +++ b/docs/scales/linear.md @@ -0,0 +1,32 @@ +# Linear scales + +A linear scale adds the same increment at every step: + +```text +value = base + step × increment +``` + +Use it for baseline grids, fixed spacing systems, and other arithmetic +progressions. + +```php +use Alto\Scale\Scale; + +$grid = Scale::linear(base: 0, increment: 8); + +$values = Scale::linear(base: 0, increment: 8)->range(-1, 4); +// [-1 => -8.0, 0 => 0.0, 1 => 8.0, 2 => 16.0, 3 => 24.0, 4 => 32.0] +``` + +Unlike logarithmic scales, a linear scale can contain zero and negative +values. + +```php +$grid->stepOf(19); // 2 +$grid->snap(19); // 16.0 +``` + +Nearest-step lookup uses normal rounding. Values exactly between two steps +follow PHP's default half-up rounding. + +The increment must be positive. An invalid increment raises `ScaleException`. diff --git a/docs/scales/modular.md b/docs/scales/modular.md new file mode 100644 index 0000000..309563a --- /dev/null +++ b/docs/scales/modular.md @@ -0,0 +1,55 @@ +# Modular scales + +A modular scale is a geometric progression: + +```text +value = base × ratio ^ step +``` + +Use it when each step should be proportionally larger or smaller than the +previous one. + +```php +use Alto\Scale\Ratio; +use Alto\Scale\Scale; + +$type = Scale::modular(base: 16, ratio: Ratio::MajorThird); + +$values = Scale::modular(base: 16, ratio: Ratio::MajorThird)->range(-2, 3); +// [10.24, 12.8, 16.0, 20.0, 25.0, 31.25], keyed from -2 to 3 +``` + +Named constructors cover common ratios: + +```php +$majorThird = Scale::majorThird(16); +$perfectFifth = Scale::perfectFifth(16); +$golden = Scale::golden(16); +``` + +`Ratio` includes thirteen established musical and typographic ratios from +`MinorSecond` (`1.067`) through `Octave` (`2.0`). A positive custom float is +also accepted. + +## Derive another scale + +Modular scales are immutable: + +```php +$compact = $type->withBase(14); +$wider = $type->withRatio(Ratio::PerfectFifth); +$shifted = $type->shift(2); +``` + +`shift(2)` returns an equivalent scale whose base is the original step two. +The object is callable, so `$type(2)` is equivalent to `$type->get(2)`. + +## Compare values + +```php +$type->areHarmonic(16, 25); // true +$type->areHarmonic(16, 24); // false +``` + +The method checks whether the logarithmic distance between two positive values +is a whole number of scale steps within the supplied epsilon. diff --git a/docs/scales/multi-strand.md b/docs/scales/multi-strand.md new file mode 100644 index 0000000..5b5cec3 --- /dev/null +++ b/docs/scales/multi-strand.md @@ -0,0 +1,29 @@ +# Multi-strand scales + +A multi-strand scale interleaves several positive bases that share one modular +ratio. Use it when two or more families of values must grow together without +collapsing into one progression. + +```php +use Alto\Scale\Ratio; +use Alto\Scale\Scale; + +$scale = Scale::strands( + bases: [12, 16], + ratio: Ratio::PerfectFifth, +); + +$scale->range(0, 5); +// [12.0, 16.0, 18.0, 24.0, 27.0, 36.0] +``` + +The bases are filtered to positive values, deduplicated, and sorted before +steps are assigned. Their original array order is therefore not significant. + +```php +$scale->stepOf(25); // 3 +$scale->snap(25); // 24.0 +``` + +The ratio must be greater than `1.0`, and at least one positive base must +remain. Invalid input raises `ScaleException`. diff --git a/phpstan.dist.neon b/phpstan.dist.neon deleted file mode 100644 index cb0c143..0000000 --- a/phpstan.dist.neon +++ /dev/null @@ -1,5 +0,0 @@ -parameters: - level: 10 - paths: - - src/ - treatPhpDocTypesAsCertain: false diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..e412539 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,7 @@ +parameters: + level: max + paths: + - src + - tests + excludePaths: + - tests/Fixtures/* diff --git a/phpunit.dist.xml b/phpunit.xml.dist similarity index 54% rename from phpunit.dist.xml rename to phpunit.xml.dist index 85047cd..7d96410 100644 --- a/phpunit.dist.xml +++ b/phpunit.xml.dist @@ -2,29 +2,20 @@ - + failOnWarning="true" + failOnNotice="true" + failOnDeprecation="true" + failOnRisky="true"> - + tests - - + src - - - - - - - diff --git a/src/AbstractScale.php b/src/AbstractScale.php index 5b6d8d7..6e2be59 100644 --- a/src/AbstractScale.php +++ b/src/AbstractScale.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/src/Exception/ScaleException.php b/src/Exception/ScaleException.php index 780516b..68f47df 100644 --- a/src/Exception/ScaleException.php +++ b/src/Exception/ScaleException.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -16,6 +16,4 @@ /** * @author Simon André */ -final class ScaleException extends \RuntimeException -{ -} +final class ScaleException extends \RuntimeException {} diff --git a/src/FibonacciScale.php b/src/FibonacciScale.php index 0faa36d..43fa38e 100644 --- a/src/FibonacciScale.php +++ b/src/FibonacciScale.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/src/LinearScale.php b/src/LinearScale.php index f359ab5..2e591ac 100644 --- a/src/LinearScale.php +++ b/src/LinearScale.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/src/ModularScale.php b/src/ModularScale.php index b3c9485..0d52328 100644 --- a/src/ModularScale.php +++ b/src/ModularScale.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/src/MultiStrandScale.php b/src/MultiStrandScale.php index 61ce47d..6e48aaf 100644 --- a/src/MultiStrandScale.php +++ b/src/MultiStrandScale.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -22,7 +22,9 @@ */ final readonly class MultiStrandScale extends AbstractScale { - /** @var list */ + /** + * @var list + */ private array $sortedBases; public float $ratio; @@ -36,7 +38,7 @@ public function __construct(array $bases, float|Ratio $ratio = Ratio::MajorThird throw new ScaleException('Ratio must be > 1.0'); } - $validBases = array_filter($bases, fn ($b) => $b > 0); + $validBases = array_filter($bases, fn($b) => $b > 0); if (empty($validBases)) { throw new ScaleException('At least one positive base required.'); } diff --git a/src/Ratio.php b/src/Ratio.php index 0b8751e..aaed071 100644 --- a/src/Ratio.php +++ b/src/Ratio.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/src/Scale.php b/src/Scale.php index c6875ae..ac4de20 100644 --- a/src/Scale.php +++ b/src/Scale.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/src/ScaleGuesser.php b/src/ScaleGuesser.php index 3db929c..2d99f9d 100644 --- a/src/ScaleGuesser.php +++ b/src/ScaleGuesser.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -20,16 +20,14 @@ */ readonly class ScaleGuesser { - public function __construct(public float $tolerance = 0.05) - { - } + public function __construct(public float $tolerance = 0.05) {} /** * @param array $values */ public function guess(array $values): ModularScale { - $clean = array_values(array_unique(array_filter($values, fn ($v) => $v > 0))); + $clean = array_values(array_unique(array_filter($values, fn($v) => $v > 0))); sort($clean); if (count($clean) < 2) { throw new ScaleException('Need 2+ values to guess.'); @@ -82,6 +80,6 @@ public function align(array $values, ?ScaleInterface $scale = null): array { $target = $scale ?? $this->guess($values); - return array_map(fn (float $v) => $v > 0 ? $target->snap($v) : $v, $values); + return array_map(fn(float $v) => $v > 0 ? $target->snap($v) : $v, $values); } } diff --git a/src/ScaleInterface.php b/src/ScaleInterface.php index c0fd838..71bb8e3 100644 --- a/src/ScaleInterface.php +++ b/src/ScaleInterface.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/src/ScaleLinter.php b/src/ScaleLinter.php index 8921bbd..b15892f 100644 --- a/src/ScaleLinter.php +++ b/src/ScaleLinter.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. @@ -18,9 +18,7 @@ */ readonly class ScaleLinter { - public function __construct(public ?ScaleInterface $scale = null, public float $tolerance = 0.05) - { - } + public function __construct(public ?ScaleInterface $scale = null, public float $tolerance = 0.05) {} /** * @param array $values @@ -56,6 +54,6 @@ public function fix(array $values): array { $scale = $this->scale ?? (new ScaleGuesser($this->tolerance))->guess($values); - return array_map(fn (float $v) => $v > 0 ? $scale->snap($v) : $v, $values); + return array_map(fn(float $v) => $v > 0 ? $scale->snap($v) : $v, $values); } } diff --git a/tests/FibonacciScaleTest.php b/tests/FibonacciScaleTest.php index 0efba56..b7d25d3 100644 --- a/tests/FibonacciScaleTest.php +++ b/tests/FibonacciScaleTest.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/tests/LinearScaleTest.php b/tests/LinearScaleTest.php index f205f4b..d12fb67 100644 --- a/tests/LinearScaleTest.php +++ b/tests/LinearScaleTest.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/tests/ModularScaleTest.php b/tests/ModularScaleTest.php index 8abd258..2a0e259 100644 --- a/tests/ModularScaleTest.php +++ b/tests/ModularScaleTest.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/tests/MultiStrandScaleTest.php b/tests/MultiStrandScaleTest.php index efff211..247a333 100644 --- a/tests/MultiStrandScaleTest.php +++ b/tests/MultiStrandScaleTest.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/tests/RatioTest.php b/tests/RatioTest.php index 554d8cd..3aa4bc1 100644 --- a/tests/RatioTest.php +++ b/tests/RatioTest.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/tests/ScaleFacadeTest.php b/tests/ScaleFacadeTest.php index 94db170..93f2a92 100644 --- a/tests/ScaleFacadeTest.php +++ b/tests/ScaleFacadeTest.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/tests/ScaleGuesserTest.php b/tests/ScaleGuesserTest.php index 03be9d8..3bbcbc5 100644 --- a/tests/ScaleGuesserTest.php +++ b/tests/ScaleGuesserTest.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code. diff --git a/tests/ScaleLinterTest.php b/tests/ScaleLinterTest.php index 388d226..f776f0e 100644 --- a/tests/ScaleLinterTest.php +++ b/tests/ScaleLinterTest.php @@ -5,7 +5,7 @@ /* * This file is part of the ALTO library. * - * © 2026–present Simon André + * © 2026-present Simon André * * For full copyright and license information, please see * the LICENSE file distributed with this source code.