mirror of
https://github.com/Intervention/image.git
synced 2025-08-29 16:50:07 +02:00
Add bezier curve drawing tool
This commit is contained in:
230
src/Drivers/Gd/Modifiers/DrawBezierModifier.php
Normal file
230
src/Drivers/Gd/Modifiers/DrawBezierModifier.php
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Intervention\Image\Drivers\Gd\Modifiers;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Intervention\Image\Exceptions\GeometryException;
|
||||||
|
use Intervention\Image\Interfaces\ImageInterface;
|
||||||
|
use Intervention\Image\Interfaces\SpecializedInterface;
|
||||||
|
use Intervention\Image\Modifiers\DrawBezierModifier as ModifiersDrawBezierModifier;
|
||||||
|
|
||||||
|
class DrawBezierModifier extends ModifiersDrawBezierModifier implements SpecializedInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException
|
||||||
|
* @throws GeometryException
|
||||||
|
*/
|
||||||
|
public function apply(ImageInterface $image): ImageInterface
|
||||||
|
{
|
||||||
|
foreach ($image as $frame) {
|
||||||
|
if ($this->drawable->count() !== 3 && $this->drawable->count() !== 4) {
|
||||||
|
throw new GeometryException('You must specify either 3 or 4 points to create a bezier curve');
|
||||||
|
}
|
||||||
|
|
||||||
|
list($polygon, $polygon_border_segments) = $this->calculateBezierPoints();
|
||||||
|
|
||||||
|
if ($this->drawable->hasBackgroundColor() || $this->drawable->hasBorder()) {
|
||||||
|
imagealphablending($frame->native(), true);
|
||||||
|
imageantialias($frame->native(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->drawable->hasBackgroundColor()) {
|
||||||
|
$background_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
|
||||||
|
$this->backgroundColor()
|
||||||
|
);
|
||||||
|
|
||||||
|
imagefilledpolygon(
|
||||||
|
$frame->native(),
|
||||||
|
$polygon,
|
||||||
|
$background_color
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 0) {
|
||||||
|
$border_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
|
||||||
|
$this->borderColor()
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->drawable->borderSize() === 1) {
|
||||||
|
imagesetthickness($frame->native(), $this->drawable->borderSize());
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($polygon); $i += 2) {
|
||||||
|
if (array_key_exists($i + 2, $polygon) && array_key_exists($i + 3, $polygon)) {
|
||||||
|
imageline(
|
||||||
|
$frame->native(),
|
||||||
|
$polygon[$i + 0],
|
||||||
|
$polygon[$i + 1],
|
||||||
|
$polygon[$i + 2],
|
||||||
|
$polygon[$i + 3],
|
||||||
|
$border_color
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$polygon_border_segments_total = count($polygon_border_segments);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $polygon_border_segments_total; $i += 1) {
|
||||||
|
imagefilledpolygon(
|
||||||
|
$frame->native(),
|
||||||
|
$polygon_border_segments[$i],
|
||||||
|
$border_color
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $image;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate interpolation points for quadratic beziers using the Bernstein polynomial form
|
||||||
|
*
|
||||||
|
* @param float $t
|
||||||
|
* @return array{'x': float, 'y': float}
|
||||||
|
*/
|
||||||
|
public function calculateQuadraticBezierInterpolationPoint(float $t = 0.05): array
|
||||||
|
{
|
||||||
|
$remainder = 1 - $t;
|
||||||
|
$control_point_1_multiplier = $remainder * $remainder;
|
||||||
|
$control_point_2_multiplier = $remainder * $t * 2;
|
||||||
|
$control_point_3_multiplier = $t * $t;
|
||||||
|
|
||||||
|
$x = (
|
||||||
|
$this->drawable->first()->x() * $control_point_1_multiplier +
|
||||||
|
$this->drawable->second()->x() * $control_point_2_multiplier +
|
||||||
|
$this->drawable->last()->x() * $control_point_3_multiplier
|
||||||
|
);
|
||||||
|
$y = (
|
||||||
|
$this->drawable->first()->y() * $control_point_1_multiplier +
|
||||||
|
$this->drawable->second()->y() * $control_point_2_multiplier +
|
||||||
|
$this->drawable->last()->y() * $control_point_3_multiplier
|
||||||
|
);
|
||||||
|
|
||||||
|
return ['x' => $x, 'y' => $y];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate interpolation points for cubic beziers using the Bernstein polynomial form
|
||||||
|
*
|
||||||
|
* @param float $t
|
||||||
|
* @return array{'x': float, 'y': float}
|
||||||
|
*/
|
||||||
|
public function calculateCubicBezierInterpolationPoint(float $t = 0.05): array
|
||||||
|
{
|
||||||
|
$remainder = 1 - $t;
|
||||||
|
$t_squared = $t * $t;
|
||||||
|
$remainder_squared = $remainder * $remainder;
|
||||||
|
$control_point_1_multiplier = $remainder_squared * $remainder;
|
||||||
|
$control_point_2_multiplier = $remainder_squared * $t * 3;
|
||||||
|
$control_point_3_multiplier = $t_squared * $remainder * 3;
|
||||||
|
$control_point_4_multiplier = $t_squared * $t;
|
||||||
|
|
||||||
|
$x = (
|
||||||
|
$this->drawable->first()->x() * $control_point_1_multiplier +
|
||||||
|
$this->drawable->second()->x() * $control_point_2_multiplier +
|
||||||
|
$this->drawable->third()->x() * $control_point_3_multiplier +
|
||||||
|
$this->drawable->last()->x() * $control_point_4_multiplier
|
||||||
|
);
|
||||||
|
$y = (
|
||||||
|
$this->drawable->first()->y() * $control_point_1_multiplier +
|
||||||
|
$this->drawable->second()->y() * $control_point_2_multiplier +
|
||||||
|
$this->drawable->third()->y() * $control_point_3_multiplier +
|
||||||
|
$this->drawable->last()->y() * $control_point_4_multiplier
|
||||||
|
);
|
||||||
|
|
||||||
|
return ['x' => $x, 'y' => $y];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the points needed to draw a quadratic or cubic bezier with optional border/stroke
|
||||||
|
*
|
||||||
|
* @throws GeometryException
|
||||||
|
* @return array{0: array<mixed>, 1: array<mixed>}
|
||||||
|
*/
|
||||||
|
public function calculateBezierPoints(): array
|
||||||
|
{
|
||||||
|
if ($this->drawable->count() !== 3 && $this->drawable->count() !== 4) {
|
||||||
|
throw new GeometryException('You must specify either 3 or 4 points to create a bezier curve');
|
||||||
|
}
|
||||||
|
|
||||||
|
$polygon = [];
|
||||||
|
$inner_polygon = [];
|
||||||
|
$outer_polygon = [];
|
||||||
|
$polygon_border_segments = [];
|
||||||
|
|
||||||
|
// define ratio t; equivalent to 5 percent distance along edge
|
||||||
|
$t = (float) 0.05;
|
||||||
|
|
||||||
|
$polygon[] = $this->drawable->first()->x();
|
||||||
|
$polygon[] = $this->drawable->first()->y();
|
||||||
|
for ($i = 0 + $t; $i < 1; $i += $t) {
|
||||||
|
if ($this->drawable->count() === 3) {
|
||||||
|
$ip = $this->calculateQuadraticBezierInterpolationPoint($i);
|
||||||
|
} elseif ($this->drawable->count() === 4) {
|
||||||
|
$ip = $this->calculateCubicBezierInterpolationPoint($i);
|
||||||
|
}
|
||||||
|
$polygon[] = (int) $ip['x'];
|
||||||
|
$polygon[] = (int) $ip['y'];
|
||||||
|
}
|
||||||
|
$polygon[] = $this->drawable->last()->x();
|
||||||
|
$polygon[] = $this->drawable->last()->y();
|
||||||
|
|
||||||
|
if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 1) {
|
||||||
|
// create the border/stroke effect by calculating two new curves with offset positions
|
||||||
|
// from the main polygon and then connecting the inner/outer curves to create separate
|
||||||
|
// 4-point polygon segments
|
||||||
|
$polygon_total_points = count($polygon);
|
||||||
|
$offset = ($this->drawable->borderSize() / 2);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $polygon_total_points; $i += 2) {
|
||||||
|
if (array_key_exists($i + 2, $polygon) && array_key_exists($i + 3, $polygon)) {
|
||||||
|
$dx = $polygon[$i + 2] - $polygon[$i];
|
||||||
|
$dy = $polygon[$i + 3] - $polygon[$i + 1];
|
||||||
|
$dxy_sqrt = ($dx * $dx + $dy * $dy) ** 0.5;
|
||||||
|
|
||||||
|
// inner polygon
|
||||||
|
$scale = $offset / $dxy_sqrt;
|
||||||
|
$ox = -$dy * $scale;
|
||||||
|
$oy = $dx * $scale;
|
||||||
|
|
||||||
|
$inner_polygon[] = $ox + $polygon[$i + 0];
|
||||||
|
$inner_polygon[] = $oy + $polygon[$i + 1];
|
||||||
|
$inner_polygon[] = $ox + $polygon[$i + 2];
|
||||||
|
$inner_polygon[] = $oy + $polygon[$i + 3];
|
||||||
|
|
||||||
|
// outer polygon
|
||||||
|
$scale = -$offset / $dxy_sqrt;
|
||||||
|
$ox = -$dy * $scale;
|
||||||
|
$oy = $dx * $scale;
|
||||||
|
|
||||||
|
$outer_polygon[] = $ox + $polygon[$i + 0];
|
||||||
|
$outer_polygon[] = $oy + $polygon[$i + 1];
|
||||||
|
$outer_polygon[] = $ox + $polygon[$i + 2];
|
||||||
|
$outer_polygon[] = $oy + $polygon[$i + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$inner_polygon_total_points = count($inner_polygon);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $inner_polygon_total_points; $i += 2) {
|
||||||
|
if (array_key_exists($i + 2, $inner_polygon) && array_key_exists($i + 3, $inner_polygon)) {
|
||||||
|
$polygon_border_segments[] = [
|
||||||
|
$inner_polygon[$i + 0],
|
||||||
|
$inner_polygon[$i + 1],
|
||||||
|
$outer_polygon[$i + 0],
|
||||||
|
$outer_polygon[$i + 1],
|
||||||
|
$outer_polygon[$i + 2],
|
||||||
|
$outer_polygon[$i + 3],
|
||||||
|
$inner_polygon[$i + 2],
|
||||||
|
$inner_polygon[$i + 3],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$polygon, $polygon_border_segments];
|
||||||
|
}
|
||||||
|
}
|
77
src/Drivers/Imagick/Modifiers/DrawBezierModifier.php
Normal file
77
src/Drivers/Imagick/Modifiers/DrawBezierModifier.php
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Intervention\Image\Drivers\Imagick\Modifiers;
|
||||||
|
|
||||||
|
use ImagickDraw;
|
||||||
|
use RuntimeException;
|
||||||
|
use Intervention\Image\Exceptions\GeometryException;
|
||||||
|
use Intervention\Image\Interfaces\ImageInterface;
|
||||||
|
use Intervention\Image\Interfaces\SpecializedInterface;
|
||||||
|
use Intervention\Image\Modifiers\DrawBezierModifier as GenericDrawBezierModifier;
|
||||||
|
|
||||||
|
class DrawBezierModifier extends GenericDrawBezierModifier implements SpecializedInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException
|
||||||
|
* @throws GeometryException
|
||||||
|
*/
|
||||||
|
public function apply(ImageInterface $image): ImageInterface
|
||||||
|
{
|
||||||
|
if ($this->drawable->count() !== 3 && $this->drawable->count() !== 4) {
|
||||||
|
throw new GeometryException('You must specify either 3 or 4 points to create a bezier curve');
|
||||||
|
}
|
||||||
|
|
||||||
|
$drawing = new ImagickDraw();
|
||||||
|
|
||||||
|
if ($this->drawable->hasBackgroundColor()) {
|
||||||
|
$background_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
|
||||||
|
$this->backgroundColor()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$background_color = 'transparent';
|
||||||
|
}
|
||||||
|
|
||||||
|
$drawing->setFillColor($background_color);
|
||||||
|
|
||||||
|
if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 0) {
|
||||||
|
$border_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
|
||||||
|
$this->borderColor()
|
||||||
|
);
|
||||||
|
|
||||||
|
$drawing->setStrokeColor($border_color);
|
||||||
|
$drawing->setStrokeWidth($this->drawable->borderSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
$drawing->pathStart();
|
||||||
|
$drawing->pathMoveToAbsolute(
|
||||||
|
$this->drawable->first()->x(),
|
||||||
|
$this->drawable->first()->y()
|
||||||
|
);
|
||||||
|
if ($this->drawable->count() === 3) {
|
||||||
|
$drawing->pathCurveToQuadraticBezierAbsolute(
|
||||||
|
$this->drawable->second()->x(),
|
||||||
|
$this->drawable->second()->y(),
|
||||||
|
$this->drawable->last()->x(),
|
||||||
|
$this->drawable->last()->y()
|
||||||
|
);
|
||||||
|
} elseif ($this->drawable->count() === 4) {
|
||||||
|
$drawing->pathCurveToAbsolute(
|
||||||
|
$this->drawable->second()->x(),
|
||||||
|
$this->drawable->second()->y(),
|
||||||
|
$this->drawable->third()->x(),
|
||||||
|
$this->drawable->third()->y(),
|
||||||
|
$this->drawable->last()->x(),
|
||||||
|
$this->drawable->last()->y()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$drawing->pathFinish();
|
||||||
|
|
||||||
|
foreach ($image as $frame) {
|
||||||
|
$frame->native()->drawImage($drawing);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $image;
|
||||||
|
}
|
||||||
|
}
|
221
src/Geometry/Bezier.php
Normal file
221
src/Geometry/Bezier.php
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Intervention\Image\Geometry;
|
||||||
|
|
||||||
|
use ArrayAccess;
|
||||||
|
use ArrayIterator;
|
||||||
|
use Countable;
|
||||||
|
use Traversable;
|
||||||
|
use IteratorAggregate;
|
||||||
|
use Intervention\Image\Geometry\Traits\HasBackgroundColor;
|
||||||
|
use Intervention\Image\Geometry\Traits\HasBorder;
|
||||||
|
use Intervention\Image\Interfaces\DrawableInterface;
|
||||||
|
use Intervention\Image\Interfaces\PointInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements IteratorAggregate<Point>
|
||||||
|
* @implements ArrayAccess<int, Point>
|
||||||
|
*/
|
||||||
|
class Bezier implements IteratorAggregate, Countable, ArrayAccess, DrawableInterface
|
||||||
|
{
|
||||||
|
use HasBorder;
|
||||||
|
use HasBackgroundColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new bezier instance
|
||||||
|
*
|
||||||
|
* @param array<Point> $points
|
||||||
|
* @param PointInterface $pivot
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected array $points = [],
|
||||||
|
protected PointInterface $pivot = new Point()
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @see DrawableInterface::position()
|
||||||
|
*/
|
||||||
|
public function position(): PointInterface
|
||||||
|
{
|
||||||
|
return $this->pivot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement iteration through all points of bezier
|
||||||
|
*
|
||||||
|
* @return Traversable<Point>
|
||||||
|
*/
|
||||||
|
public function getIterator(): Traversable
|
||||||
|
{
|
||||||
|
return new ArrayIterator($this->points);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return current pivot point
|
||||||
|
*
|
||||||
|
* @return PointInterface
|
||||||
|
*/
|
||||||
|
public function pivot(): PointInterface
|
||||||
|
{
|
||||||
|
return $this->pivot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change pivot point to given point
|
||||||
|
*
|
||||||
|
* @param Point $pivot
|
||||||
|
* @return Bezier
|
||||||
|
*/
|
||||||
|
public function setPivot(Point $pivot): self
|
||||||
|
{
|
||||||
|
$this->pivot = $pivot;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return first control point of bezier
|
||||||
|
*
|
||||||
|
* @return ?Point
|
||||||
|
*/
|
||||||
|
public function first(): ?Point
|
||||||
|
{
|
||||||
|
if ($point = reset($this->points)) {
|
||||||
|
return $point;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return second control point of bezier
|
||||||
|
*
|
||||||
|
* @return ?Point
|
||||||
|
*/
|
||||||
|
public function second(): ?Point
|
||||||
|
{
|
||||||
|
if (array_key_exists(1, $this->points)) {
|
||||||
|
return $this->points[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return third control point of bezier
|
||||||
|
*
|
||||||
|
* @return ?Point
|
||||||
|
*/
|
||||||
|
public function third(): ?Point
|
||||||
|
{
|
||||||
|
if (array_key_exists(2, $this->points)) {
|
||||||
|
return $this->points[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return last control point of bezier
|
||||||
|
*
|
||||||
|
* @return ?Point
|
||||||
|
*/
|
||||||
|
public function last(): ?Point
|
||||||
|
{
|
||||||
|
if ($point = end($this->points)) {
|
||||||
|
return $point;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return bezier's point count
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function count(): int
|
||||||
|
{
|
||||||
|
return count($this->points);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if point exists at given offset
|
||||||
|
*
|
||||||
|
* @param mixed $offset
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function offsetExists($offset): bool
|
||||||
|
{
|
||||||
|
return array_key_exists($offset, $this->points);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return point at given offset
|
||||||
|
*
|
||||||
|
* @param mixed $offset
|
||||||
|
* @return Point
|
||||||
|
*/
|
||||||
|
public function offsetGet($offset): mixed
|
||||||
|
{
|
||||||
|
return $this->points[$offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set point at given offset
|
||||||
|
*
|
||||||
|
* @param mixed $offset
|
||||||
|
* @param Point $value
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function offsetSet($offset, $value): void
|
||||||
|
{
|
||||||
|
$this->points[$offset] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unset offset at given offset
|
||||||
|
*
|
||||||
|
* @param mixed $offset
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function offsetUnset($offset): void
|
||||||
|
{
|
||||||
|
unset($this->points[$offset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add given point to bezier
|
||||||
|
*
|
||||||
|
* @param Point $point
|
||||||
|
* @return Bezier
|
||||||
|
*/
|
||||||
|
public function addPoint(Point $point): self
|
||||||
|
{
|
||||||
|
$this->points[] = $point;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return array of all x/y values of all points of bezier
|
||||||
|
*
|
||||||
|
* @return array<int>
|
||||||
|
*/
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
$coordinates = [];
|
||||||
|
foreach ($this->points as $point) {
|
||||||
|
$coordinates[] = $point->x();
|
||||||
|
$coordinates[] = $point->y();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $coordinates;
|
||||||
|
}
|
||||||
|
}
|
79
src/Geometry/Factories/BezierFactory.php
Normal file
79
src/Geometry/Factories/BezierFactory.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Intervention\Image\Geometry\Factories;
|
||||||
|
|
||||||
|
use Intervention\Image\Geometry\Point;
|
||||||
|
use Intervention\Image\Geometry\Bezier;
|
||||||
|
|
||||||
|
class BezierFactory
|
||||||
|
{
|
||||||
|
protected Bezier $bezier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new factory instance
|
||||||
|
*
|
||||||
|
* @param callable|Bezier $init
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(callable|Bezier $init)
|
||||||
|
{
|
||||||
|
$this->bezier = is_a($init, Bezier::class) ? $init : new Bezier([]);
|
||||||
|
|
||||||
|
if (is_callable($init)) {
|
||||||
|
$init($this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a point to the bezier to be produced
|
||||||
|
*
|
||||||
|
* @param int $x
|
||||||
|
* @param int $y
|
||||||
|
* @return BezierFactory
|
||||||
|
*/
|
||||||
|
public function point(int $x, int $y): self
|
||||||
|
{
|
||||||
|
$this->bezier->addPoint(new Point($x, $y));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the background color of the bezier to be produced
|
||||||
|
*
|
||||||
|
* @param mixed $color
|
||||||
|
* @return BezierFactory
|
||||||
|
*/
|
||||||
|
public function background(mixed $color): self
|
||||||
|
{
|
||||||
|
$this->bezier->setBackgroundColor($color);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the border color & border size of the bezier to be produced
|
||||||
|
*
|
||||||
|
* @param mixed $color
|
||||||
|
* @param int $size
|
||||||
|
* @return BezierFactory
|
||||||
|
*/
|
||||||
|
public function border(mixed $color, int $size = 1): self
|
||||||
|
{
|
||||||
|
$this->bezier->setBorder($color, $size);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produce the bezier
|
||||||
|
*
|
||||||
|
* @return Bezier
|
||||||
|
*/
|
||||||
|
public function __invoke(): Bezier
|
||||||
|
{
|
||||||
|
return $this->bezier;
|
||||||
|
}
|
||||||
|
}
|
@@ -27,6 +27,7 @@ use Intervention\Image\Encoders\PngEncoder;
|
|||||||
use Intervention\Image\Encoders\TiffEncoder;
|
use Intervention\Image\Encoders\TiffEncoder;
|
||||||
use Intervention\Image\Encoders\WebpEncoder;
|
use Intervention\Image\Encoders\WebpEncoder;
|
||||||
use Intervention\Image\Exceptions\EncoderException;
|
use Intervention\Image\Exceptions\EncoderException;
|
||||||
|
use Intervention\Image\Geometry\Factories\BezierFactory;
|
||||||
use Intervention\Image\Geometry\Factories\CircleFactory;
|
use Intervention\Image\Geometry\Factories\CircleFactory;
|
||||||
use Intervention\Image\Geometry\Factories\EllipseFactory;
|
use Intervention\Image\Geometry\Factories\EllipseFactory;
|
||||||
use Intervention\Image\Geometry\Factories\LineFactory;
|
use Intervention\Image\Geometry\Factories\LineFactory;
|
||||||
@@ -58,6 +59,7 @@ use Intervention\Image\Modifiers\ColorspaceModifier;
|
|||||||
use Intervention\Image\Modifiers\ContainModifier;
|
use Intervention\Image\Modifiers\ContainModifier;
|
||||||
use Intervention\Image\Modifiers\ContrastModifier;
|
use Intervention\Image\Modifiers\ContrastModifier;
|
||||||
use Intervention\Image\Modifiers\CropModifier;
|
use Intervention\Image\Modifiers\CropModifier;
|
||||||
|
use Intervention\Image\Modifiers\DrawBezierModifier;
|
||||||
use Intervention\Image\Modifiers\DrawEllipseModifier;
|
use Intervention\Image\Modifiers\DrawEllipseModifier;
|
||||||
use Intervention\Image\Modifiers\DrawLineModifier;
|
use Intervention\Image\Modifiers\DrawLineModifier;
|
||||||
use Intervention\Image\Modifiers\DrawPixelModifier;
|
use Intervention\Image\Modifiers\DrawPixelModifier;
|
||||||
@@ -874,6 +876,20 @@ final class Image implements ImageInterface
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @see ImageInterface::drawBezier()
|
||||||
|
*/
|
||||||
|
public function drawBezier(callable $init): ImageInterface
|
||||||
|
{
|
||||||
|
return $this->modify(
|
||||||
|
new DrawBezierModifier(
|
||||||
|
call_user_func(new BezierFactory($init)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*
|
*
|
||||||
|
@@ -749,6 +749,16 @@ interface ImageInterface extends IteratorAggregate, Countable
|
|||||||
*/
|
*/
|
||||||
public function drawLine(callable $init): self;
|
public function drawLine(callable $init): self;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a bezier curve on the current image
|
||||||
|
*
|
||||||
|
* @link
|
||||||
|
* @param callable $init
|
||||||
|
* @throws RuntimeException
|
||||||
|
* @return ImageInterface
|
||||||
|
*/
|
||||||
|
public function drawBezier(callable $init): self;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode image to given media (mime) type. If no type is given the image
|
* Encode image to given media (mime) type. If no type is given the image
|
||||||
* will be encoded to the format of the originally read image.
|
* will be encoded to the format of the originally read image.
|
||||||
|
20
src/Modifiers/DrawBezierModifier.php
Normal file
20
src/Modifiers/DrawBezierModifier.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Intervention\Image\Modifiers;
|
||||||
|
|
||||||
|
use Intervention\Image\Geometry\Bezier;
|
||||||
|
use Intervention\Image\Interfaces\DrawableInterface;
|
||||||
|
|
||||||
|
class DrawBezierModifier extends AbstractDrawModifier
|
||||||
|
{
|
||||||
|
public function __construct(public Bezier $drawable)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function drawable(): DrawableInterface
|
||||||
|
{
|
||||||
|
return $this->drawable;
|
||||||
|
}
|
||||||
|
}
|
33
tests/Unit/Drivers/Gd/Modifiers/DrawBezierModifierTest.php
Normal file
33
tests/Unit/Drivers/Gd/Modifiers/DrawBezierModifierTest.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Intervention\Image\Tests\Unit\Drivers\Gd\Modifiers;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
|
||||||
|
use Intervention\Image\Modifiers\DrawBezierModifier;
|
||||||
|
use Intervention\Image\Geometry\Point;
|
||||||
|
use Intervention\Image\Geometry\Bezier;
|
||||||
|
use Intervention\Image\Tests\GdTestCase;
|
||||||
|
|
||||||
|
#[RequiresPhpExtension('gd')]
|
||||||
|
#[CoversClass(\Intervention\Image\Modifiers\DrawPixelModifier::class)]
|
||||||
|
#[CoversClass(\Intervention\Image\Drivers\Gd\Modifiers\DrawPixelModifier::class)]
|
||||||
|
final class DrawBezierModifierTest extends GdTestCase
|
||||||
|
{
|
||||||
|
public function testApply(): void
|
||||||
|
{
|
||||||
|
$image = $this->readTestImage('trim.png');
|
||||||
|
$this->assertEquals('00aef0', $image->pickColor(14, 14)->toHex());
|
||||||
|
$drawable = new Bezier([
|
||||||
|
new Point(0, 0),
|
||||||
|
new Point(15, 0),
|
||||||
|
new Point(15, 15),
|
||||||
|
new Point(0, 15)
|
||||||
|
]);
|
||||||
|
$drawable->setBackgroundColor('b53717');
|
||||||
|
$image->modify(new DrawBezierModifier($drawable));
|
||||||
|
$this->assertEquals('b53717', $image->pickColor(5, 5)->toHex());
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Intervention\Image\Tests\Unit\Drivers\Imagick\Modifiers;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
|
||||||
|
use Intervention\Image\Modifiers\DrawBezierModifier;
|
||||||
|
use Intervention\Image\Geometry\Point;
|
||||||
|
use Intervention\Image\Geometry\Bezier;
|
||||||
|
use Intervention\Image\Tests\ImagickTestCase;
|
||||||
|
|
||||||
|
#[RequiresPhpExtension('imagick')]
|
||||||
|
#[CoversClass(\Intervention\Image\Modifiers\DrawBezierModifier::class)]
|
||||||
|
#[CoversClass(\Intervention\Image\Drivers\Imagick\Modifiers\DrawBezierModifier::class)]
|
||||||
|
final class DrawBezierModifierTest extends ImagickTestCase
|
||||||
|
{
|
||||||
|
public function testApply(): void
|
||||||
|
{
|
||||||
|
$image = $this->readTestImage('trim.png');
|
||||||
|
$this->assertEquals('00aef0', $image->pickColor(14, 14)->toHex());
|
||||||
|
$drawable = new Bezier([
|
||||||
|
new Point(0, 0),
|
||||||
|
new Point(15, 0),
|
||||||
|
new Point(15, 15),
|
||||||
|
new Point(0, 15)
|
||||||
|
]);
|
||||||
|
$drawable->setBackgroundColor('b53717');
|
||||||
|
$image->modify(new DrawBezierModifier($drawable));
|
||||||
|
$this->assertEquals('b53717', $image->pickColor(5, 5)->toHex());
|
||||||
|
}
|
||||||
|
}
|
177
tests/Unit/Geometry/BezierTest.php
Normal file
177
tests/Unit/Geometry/BezierTest.php
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Intervention\Image\Tests\Unit\Geometry;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use Intervention\Image\Geometry\Point;
|
||||||
|
use Intervention\Image\Geometry\Bezier;
|
||||||
|
use Intervention\Image\Tests\BaseTestCase;
|
||||||
|
|
||||||
|
#[CoversClass(\Intervention\Image\Geometry\Bezier::class)]
|
||||||
|
final class BezierTest extends BaseTestCase
|
||||||
|
{
|
||||||
|
public function testConstructor(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier([]);
|
||||||
|
$this->assertInstanceOf(Bezier::class, $bezier);
|
||||||
|
$this->assertEquals(0, $bezier->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCount(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier([
|
||||||
|
new Point(),
|
||||||
|
new Point(),
|
||||||
|
new Point(),
|
||||||
|
new Point()
|
||||||
|
]);
|
||||||
|
$this->assertEquals(4, $bezier->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testArrayAccess(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier([
|
||||||
|
new Point(),
|
||||||
|
new Point(),
|
||||||
|
new Point(),
|
||||||
|
new Point()
|
||||||
|
]);
|
||||||
|
$this->assertInstanceOf(Point::class, $bezier[0]);
|
||||||
|
$this->assertInstanceOf(Point::class, $bezier[1]);
|
||||||
|
$this->assertInstanceOf(Point::class, $bezier[2]);
|
||||||
|
$this->assertInstanceOf(Point::class, $bezier[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddPoint(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier([
|
||||||
|
new Point(),
|
||||||
|
new Point()
|
||||||
|
]);
|
||||||
|
$this->assertEquals(2, $bezier->count());
|
||||||
|
$result = $bezier->addPoint(new Point());
|
||||||
|
$this->assertEquals(3, $bezier->count());
|
||||||
|
$this->assertInstanceOf(Bezier::class, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFirst(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier([
|
||||||
|
new Point(50, 45),
|
||||||
|
new Point(100, -49),
|
||||||
|
new Point(-100, 100),
|
||||||
|
new Point(200, 300),
|
||||||
|
]);
|
||||||
|
$this->assertEquals(50, $bezier->first()->x());
|
||||||
|
$this->assertEquals(45, $bezier->first()->y());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFirstEmpty(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier();
|
||||||
|
$this->assertNull($bezier->first());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSecond(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier([
|
||||||
|
new Point(50, 45),
|
||||||
|
new Point(100, -49),
|
||||||
|
new Point(-100, 100),
|
||||||
|
new Point(200, 300),
|
||||||
|
]);
|
||||||
|
$this->assertEquals(100, $bezier->second()->x());
|
||||||
|
$this->assertEquals(-49, $bezier->second()->y());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSecondEmpty(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier();
|
||||||
|
$this->assertNull($bezier->second());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThird(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier([
|
||||||
|
new Point(50, 45),
|
||||||
|
new Point(100, -49),
|
||||||
|
new Point(-100, 100),
|
||||||
|
new Point(200, 300),
|
||||||
|
]);
|
||||||
|
$this->assertEquals(-100, $bezier->third()->x());
|
||||||
|
$this->assertEquals(100, $bezier->third()->y());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThirdEmpty(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier();
|
||||||
|
$this->assertNull($bezier->third());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLast(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier([
|
||||||
|
new Point(50, 45),
|
||||||
|
new Point(100, -49),
|
||||||
|
new Point(-100, 100),
|
||||||
|
new Point(200, 300),
|
||||||
|
]);
|
||||||
|
$this->assertEquals(200, $bezier->last()->x());
|
||||||
|
$this->assertEquals(300, $bezier->last()->y());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLastEmpty(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier();
|
||||||
|
$this->assertNull($bezier->last());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOffsetExists(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier();
|
||||||
|
$this->assertFalse($bezier->offsetExists(0));
|
||||||
|
$this->assertFalse($bezier->offsetExists(1));
|
||||||
|
$bezier->addPoint(new Point(0, 0));
|
||||||
|
$this->assertTrue($bezier->offsetExists(0));
|
||||||
|
$this->assertFalse($bezier->offsetExists(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOffsetSetUnset(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier();
|
||||||
|
$bezier->offsetSet(0, new Point());
|
||||||
|
$bezier->offsetSet(2, new Point());
|
||||||
|
$this->assertTrue($bezier->offsetExists(0));
|
||||||
|
$this->assertFalse($bezier->offsetExists(1));
|
||||||
|
$this->assertTrue($bezier->offsetExists(2));
|
||||||
|
$bezier->offsetUnset(2);
|
||||||
|
$this->assertTrue($bezier->offsetExists(0));
|
||||||
|
$this->assertFalse($bezier->offsetExists(1));
|
||||||
|
$this->assertFalse($bezier->offsetExists(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSetPivotPoint(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier();
|
||||||
|
$this->assertInstanceOf(Point::class, $bezier->pivot());
|
||||||
|
$this->assertEquals(0, $bezier->pivot()->x());
|
||||||
|
$this->assertEquals(0, $bezier->pivot()->y());
|
||||||
|
$result = $bezier->setPivot(new Point(12, 34));
|
||||||
|
$this->assertInstanceOf(Bezier::class, $result);
|
||||||
|
$this->assertEquals(12, $bezier->pivot()->x());
|
||||||
|
$this->assertEquals(34, $bezier->pivot()->y());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testToArray(): void
|
||||||
|
{
|
||||||
|
$bezier = new Bezier([
|
||||||
|
new Point(50, 50),
|
||||||
|
new Point(100, 50),
|
||||||
|
new Point(-50, -100),
|
||||||
|
new Point(50, 100),
|
||||||
|
]);
|
||||||
|
$this->assertEquals([50, 50, 100, 50, -50, -100, 50, 100], $bezier->toArray());
|
||||||
|
}
|
||||||
|
}
|
31
tests/Unit/Geometry/Factories/BezierFactoryTest.php
Normal file
31
tests/Unit/Geometry/Factories/BezierFactoryTest.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Intervention\Image\Tests\Unit\Geometry\Factories;
|
||||||
|
|
||||||
|
use Intervention\Image\Geometry\Factories\BezierFactory;
|
||||||
|
use Intervention\Image\Geometry\Bezier;
|
||||||
|
use Intervention\Image\Tests\BaseTestCase;
|
||||||
|
|
||||||
|
final class BezierFactoryTest extends BaseTestCase
|
||||||
|
{
|
||||||
|
public function testFactoryCallback(): void
|
||||||
|
{
|
||||||
|
$factory = new BezierFactory(function ($bezier) {
|
||||||
|
$bezier->background('f00');
|
||||||
|
$bezier->border('ff0', 10);
|
||||||
|
$bezier->point(300, 260);
|
||||||
|
$bezier->point(150, 335);
|
||||||
|
$bezier->point(300, 410);
|
||||||
|
});
|
||||||
|
|
||||||
|
$bezier = $factory();
|
||||||
|
$this->assertInstanceOf(Bezier::class, $bezier);
|
||||||
|
$this->assertTrue($bezier->hasBackgroundColor());
|
||||||
|
$this->assertEquals('f00', $bezier->backgroundColor());
|
||||||
|
$this->assertEquals('ff0', $bezier->borderColor());
|
||||||
|
$this->assertEquals(10, $bezier->borderSize());
|
||||||
|
$this->assertEquals(3, $bezier->count());
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user