Skip to content

Commit ebec94d

Browse files
authored
feat: Added repeat option to disable loop (#181)
1 parent d65ccb2 commit ebec94d

File tree

9 files changed

+182
-42
lines changed

9 files changed

+182
-42
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ Feel free to [open a PR](https://github.com/DenverCoder1/readme-typing-svg/issue
9898
| `multiline` | `true` to wrap lines or `false` to retype on one line (default: `false`) | boolean | `true` or `false` |
9999
| `duration` | Duration of the printing of a single line in milliseconds (default: `5000`) | integer | Any positive number |
100100
| `pause` | Duration of the pause between lines in milliseconds (default: `0`) | integer | Any non-negative number |
101+
| `repeat` | `true` to loop around to the first line after the last (default: `true`) | boolean | `true` or `false` |
101102

102103
## 📤 Deploying it on your own
103104

src/demo/index.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,23 +83,29 @@ function gtag() {
8383
<input class="param jscolor jscolor-active" id="background" name="background" alt="Background color" data-jscolor="{ format: 'hexa' }" value="#00000000">
8484

8585
<label for="center">Horizontally Centered</label>
86-
<select class="param" id="center" name="center" alt="Horizontally Centered" value="false">
86+
<select class="param" id="center" name="center" alt="Horizontally Centered">
8787
<option>false</option>
8888
<option>true</option>
8989
</select>
9090

9191
<label for="vCenter">Vertically Centered</label>
92-
<select class="param" id="vCenter" name="vCenter" alt="Vertically Centered" value="false">
92+
<select class="param" id="vCenter" name="vCenter" alt="Vertically Centered">
9393
<option>false</option>
9494
<option>true</option>
9595
</select>
9696

9797
<label for="multiline">Multiline</label>
98-
<select class="param" id="multiline" name="multiline" alt="Multiline" value="false">
98+
<select class="param" id="multiline" name="multiline" alt="Multiline">
9999
<option value="false">Type sentences on one line</option>
100100
<option value="true">Each sentence on a new line</option>
101101
</select>
102102

103+
<label for="repeat">Repeat</label>
104+
<select class="param" id="repeat" name="repeat" alt="Repeat">
105+
<option>true</option>
106+
<option>false</option>
107+
</select>
108+
103109
<label for="dimensions" title="Width ✕ Height">Width ✕ Height</label>
104110
<span id="dimensions">
105111
<input class="param inline" type="number" id="width" name="width" alt="Width (px)" placeholder="435" value="435">

src/demo/js/script.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ let preview = {
1313
height: "50",
1414
duration: "5000",
1515
pause: "0",
16+
repeat: "true",
1617
},
1718
dummyText: [
1819
"The five boxing wizards jump quickly",

src/models/RendererModel.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ class RendererModel
4646
/** @var int $pause pause duration between lines in milliseconds */
4747
public $pause;
4848

49+
/** @var bool $repeat Whether to loop around to the first line after the last */
50+
public $repeat;
51+
4952
/** @var string $fontCSS CSS required for displaying the selected font */
5053
public $fontCSS;
5154

@@ -66,6 +69,7 @@ class RendererModel
6669
"multiline" => "false",
6770
"duration" => "5000",
6871
"pause" => "0",
72+
"repeat" => "true",
6973
];
7074

7175
/**
@@ -90,6 +94,7 @@ public function __construct($template, $params)
9094
$this->multiline = $this->checkBoolean($params["multiline"] ?? $this->DEFAULTS["multiline"]);
9195
$this->duration = $this->checkNumberPositive($params["duration"] ?? $this->DEFAULTS["duration"], "duration");
9296
$this->pause = $this->checkNumberNonNegative($params["pause"] ?? $this->DEFAULTS["pause"], "pause");
97+
$this->repeat = $this->checkBoolean($params["repeat"] ?? $this->DEFAULTS["repeat"]);
9398
$this->fontCSS = $this->fetchFontCSS($this->font, $this->weight, $params["lines"]);
9499
}
95100

src/templates/main.php

Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,62 @@
55
style='background-color: <?= $background ?>;'
66
width='<?= $width ?>px' height='<?= $height ?>px'>
77

8-
<?= preg_replace("/\n/", "\n\t", $fontCSS) ?>
8+
<?= $fontCSS ?>
99

10-
<?php $previousId = "d" . (count($lines) - 1); ?>
11-
<?php for ($i = 0; $i < count($lines); ++$i): ?>
12-
<path id='path<?= $i ?>'>
13-
<?php if (!$multiline): ?>
14-
<animate id='d<?= $i ?>' attributeName='d' begin='<?= ($i == 0 ? "0s;" : "") .
15-
$previousId ?>.end' dur='<?= $duration + $pause ?>ms'
16-
values='m0,<?= $height / 2 ?> h0 ; m0,<?= $height / 2 ?> h<?= $width ?> ; m0,<?= $height /
17-
2 ?> h<?= $width ?> ; m0,<?= $height / 2 ?> h0'
18-
keyTimes='0;<?= (0.8 * $duration) / ($duration + $pause) ?>;<?= (0.8 * $duration + $pause) /
19-
($duration + $pause) ?>;1' />
20-
<?php else: ?>
21-
<?php $lineHeight = $size + 5; ?>
22-
<animate id='d<?= $i ?>' attributeName='d' dur='<?= ($duration + $pause) * ($i + 1) ?>ms' fill="freeze"
23-
begin='0s;<?= "d" . (count($lines) - 1) ?>.end' keyTimes='0;<?= $i / ($i + 1) ?>;<?= $i / ($i + 1) +
24-
$duration / (($duration + $pause) * ($i + 1)) ?>;1'
25-
values='m0,<?= ($i + 1) * $lineHeight ?> h0 ; m0,<?= ($i + 1) * $lineHeight ?> h0 ; m0,<?= ($i + 1) *
26-
$lineHeight ?> h<?= $width ?> ; m0,<?= ($i + 1) * $lineHeight ?> h<?= $width ?>' />
27-
<?php endif; ?>
28-
</path>
10+
<?php $lastLineIndex = count($lines) - 1; ?>
11+
<?php for ($i = 0; $i <= $lastLineIndex; ++$i): ?>
12+
<path id='path<?= $i ?>'>
13+
<?php if (!$multiline): ?>
14+
<!-- Single line -->
15+
<?php
16+
// start after previous line
17+
$begin = "d" . ($i - 1) . ".end";
18+
if ($i == 0) {
19+
// if this is the first line, start at 0 seconds
20+
// and also after the last line if repeat is true
21+
$begin = $repeat ? "0s;d$lastLineIndex.end" : "0s";
22+
}
23+
// don't delete text after typing the last line if repeat is false
24+
$freeze = !$repeat && $i == $lastLineIndex;
25+
// empty line values
26+
$yOffset = $height / 2;
27+
$emptyLine = "m0,$yOffset h0";
28+
$fullLine = "m0,$yOffset h$width";
29+
$values = [$emptyLine, $fullLine, $fullLine, $freeze ? $fullLine : $emptyLine];
30+
// keyTimes for the animation
31+
$keyTimes = [
32+
"0",
33+
(0.8 * $duration) / ($duration + $pause),
34+
(0.8 * $duration + $pause) / ($duration + $pause),
35+
"1",
36+
];
37+
?>
38+
<animate id='d<?= $i ?>' attributeName='d' begin='<?= $begin ?>'
39+
dur='<?= $duration + $pause ?>ms' fill='<?= $freeze ? "freeze" : "remove" ?>'
40+
values='<?= implode(" ; ", $values) ?>' keyTimes='<?= implode(";", $keyTimes) ?>' />
41+
<?php else: ?>
42+
<!-- Multiline -->
43+
<?php
44+
$nextIndex = $i + 1;
45+
$lineHeight = $size + 5;
46+
$lineDuration = ($duration + $pause) * $nextIndex;
47+
$yOffset = $nextIndex * $lineHeight;
48+
$emptyLine = "m0,$yOffset h0";
49+
$fullLine = "m0,$yOffset h$width";
50+
$values = [$emptyLine, $emptyLine, $fullLine, $fullLine];
51+
$keyTimes = ["0", $i / $nextIndex, $i / $nextIndex + $duration / $lineDuration, "1"];
52+
?>
53+
<animate id='d<?= $i ?>' attributeName='d' begin='0s<?= $repeat ? ";d$lastLineIndex.end" : "" ?>'
54+
dur='<?= $lineDuration ?>ms' fill="freeze"
55+
values='<?= implode(" ; ", $values) ?>' keyTimes='<?= implode(";", $keyTimes) ?>' />
56+
<?php endif; ?>
57+
</path>
2958
<text font-family='"<?= $font ?>", monospace' fill='<?= $color ?>' font-size='<?= $size ?>'
30-
<?php if ($vCenter): ?>
31-
dominant-baseline='middle'
32-
<?php else: ?>
33-
dominant-baseline='auto'
34-
<?php endif; ?>
35-
<?php if ($center): ?>
36-
x='50%' text-anchor='middle'>
37-
<?php else: ?>
38-
x='0%' text-anchor='start'>
39-
<?php endif; ?>
59+
dominant-baseline='<?= $vCenter ? "middle" : "auto" ?>'
60+
x='<?= $center ? "50%" : "0%" ?>' text-anchor='<?= $center ? "middle" : "start" ?>'>
4061
<textPath xlink:href='#path<?= $i ?>'>
4162
<?= $lines[$i] . "\n" ?>
4263
</textPath>
4364
</text>
44-
45-
<?php $previousId = "d" . $i; ?>
4665
<?php endfor; ?>
4766
</svg>

src/views/RendererView.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public function render()
4040
"fontCSS" => $this->model->fontCSS,
4141
"duration" => $this->model->duration,
4242
"pause" => $this->model->pause,
43+
"repeat" => $this->model->repeat,
4344
]);
4445
// render SVG with output buffering
4546
ob_start();

tests/OptionsTest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,33 @@ public function testInvalidPause(): void
287287
];
288288
print_r(new RendererModel("src/templates/main.php", $params));
289289
}
290+
291+
/**
292+
* Test repeat set to true, false, other
293+
*/
294+
public function testRepeat(): void
295+
{
296+
// not set
297+
$params = [
298+
"lines" => "text",
299+
];
300+
$model = new RendererModel("src/templates/main.php", $params);
301+
$this->assertEquals(true, $model->repeat);
302+
303+
// true
304+
$params = [
305+
"lines" => "text",
306+
"repeat" => "true",
307+
];
308+
$model = new RendererModel("src/templates/main.php", $params);
309+
$this->assertEquals(true, $model->repeat);
310+
311+
// other / false
312+
$params = [
313+
"lines" => "text",
314+
"repeat" => "other",
315+
];
316+
$model = new RendererModel("src/templates/main.php", $params);
317+
$this->assertEquals(false, $model->repeat);
318+
}
290319
}

tests/RendererTest.php

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,24 @@
88

99
final class RendererTest extends TestCase
1010
{
11+
/**
12+
* Static method to assert strings are equal while ignoring whitespace
13+
*
14+
* @param string $expected
15+
* @param string $actual
16+
*/
17+
public static function compareNoCommentsOrWhitespace(string $expected, string $actual)
18+
{
19+
// remove comments and whitespace
20+
$expected = preg_replace("/\s+/", " ", preg_replace("/<!--.*?-->/s", " ", $expected));
21+
$actual = preg_replace("/\s+/", " ", preg_replace("/<!--.*?-->/s", " ", $actual));
22+
// add newlines to make it easier to debug
23+
$expected = str_replace(">", ">\n", $expected);
24+
$actual = str_replace(">", ">\n", $actual);
25+
// assert strings are equal
26+
self::assertSame($expected, $actual);
27+
}
28+
1129
/**
1230
* Test normal card render
1331
*/
@@ -26,7 +44,9 @@ public function testCardRender(): void
2644
"height" => "50",
2745
];
2846
$controller = new RendererController($params);
29-
$this->assertStringEqualsFile("tests/svg/test_normal.svg", $controller->render());
47+
$expectedSVG = file_get_contents("tests/svg/test_normal.svg");
48+
$actualSVG = $controller->render();
49+
$this->compareNoCommentsOrWhitespace($expectedSVG, $actualSVG);
3050
}
3151

3252
public function testMultilineCardRender(): void
@@ -134,7 +154,9 @@ public function testLineTrimming(): void
134154
"height" => "50",
135155
];
136156
$controller = new RendererController($params);
137-
$this->assertStringEqualsFile("tests/svg/test_normal.svg", $controller->render());
157+
$expectedSVG = file_get_contents("tests/svg/test_normal.svg");
158+
$actualSVG = $controller->render();
159+
$this->compareNoCommentsOrWhitespace($expectedSVG, $actualSVG);
138160
}
139161

140162
/**
@@ -205,4 +227,57 @@ public function testPauseMultiline(): void
205227
$this->assertStringContainsString("dur='12000ms'", $controller->render());
206228
$this->assertStringContainsString("keyTimes='0;0.5;0.91666666666667;1'", $controller->render());
207229
}
230+
231+
/**
232+
* Test repeat set to false
233+
*/
234+
public function testRepeatFalse(): void
235+
{
236+
$params = [
237+
"lines" => implode(";", [
238+
"Full-stack web and app developer",
239+
"Self-taught UI/UX Designer",
240+
"10+ years of coding experience",
241+
"Always learning new things",
242+
]),
243+
"center" => "true",
244+
"vCenter" => "true",
245+
"width" => "380",
246+
"height" => "50",
247+
"repeat" => "false",
248+
];
249+
$controller = new RendererController($params);
250+
$actualSVG = preg_replace("/\s+/", " ", $controller->render());
251+
$this->assertStringContainsString("begin='0s'", $actualSVG);
252+
$this->assertStringContainsString(
253+
"begin='d2.end' dur='5000ms' fill='freeze' values='m0,25 h0 ; m0,25 h380 ; m0,25 h380 ; m0,25 h380'",
254+
$actualSVG
255+
);
256+
$this->assertStringNotContainsString(";d3.end", $actualSVG);
257+
}
258+
259+
/**
260+
* Test repeat set to false on multiline card
261+
*/
262+
public function testRepeatFalseMultiline(): void
263+
{
264+
$params = [
265+
"lines" => implode(";", [
266+
"Full-stack web and app developer",
267+
"Self-taught UI/UX Designer",
268+
"10+ years of coding experience",
269+
"Always learning new things",
270+
]),
271+
"center" => "true",
272+
"vCenter" => "true",
273+
"width" => "380",
274+
"height" => "200",
275+
"multiline" => "true",
276+
"repeat" => "false",
277+
];
278+
$controller = new RendererController($params);
279+
$actualSVG = preg_replace("/\s+/", " ", $controller->render());
280+
$this->assertStringContainsString("begin='0s'", $actualSVG);
281+
$this->assertStringNotContainsString(";d3.end", $actualSVG);
282+
}
208283
}

tests/svg/test_normal.svg

Lines changed: 8 additions & 5 deletions
Loading

0 commit comments

Comments
 (0)