--- /dev/null
+@font-face {
+ font-family: 'Fira Sans';
+ font-style: normal;
+ font-weight: 300;
+ src: local('Fira Sans OT Light'), local('Fira Sans Light'), url(FiraSans-Light.woff) format('woff');
+}
+
+@font-face {
+ font-family: 'Fira Sans';
+ font-style: italic;
+ font-weight: 300;
+ src: local('Fira Sans OT Light Italic'), local('Fira Sans Light Italic'), url(FiraSans-LightItalic.woff) format('woff');
+}
+
+@font-face {
+ font-family: 'Fira Sans';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Fira Sans OT'), local('Fira Sans'), url(FiraSans-Regular.woff) format('woff');
+}
+
+@font-face {
+ font-family: 'Fira Sans';
+ font-style: normal;
+ font-weight: 700;
+ src: local('Fira Sans OT Bold'), local('Fira Sans Bold'), url(FiraSans-Bold.woff) format('woff');
+}
+
+@font-face {
+ font-family: 'Fira Sans';
+ font-style: italic;
+ font-weight: 400;
+ src: local('Fira Sans OT Italic'), local('Fira Sans Italic'), url(FiraSans-RegularItalic.woff) format('woff');
+}
+
+@font-face {
+ font-family: 'Fira Sans';
+ font-style: italic;
+ font-weight: 700;
+ src: local('Fira Sans OT Bold Italic'), local('Fira Sans Bold Italic'), url(FiraSans-BoldItalic.woff) format('woff');
+}
+
+@font-face {
+ font-family: 'Fira Mono';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Fira Mono OT'), local('Fira Mono'), url(FiraMono-Regular.woff) format('woff');
+}
+
+@font-face {
+ font-family: 'Fira Mono';
+ font-style: normal;
+ font-weight: 700;
+ src: local('Fira Mono OT Bold'), local('Fira Mono Bold'), url(FiraMono-Bold.woff) format('woff');
+}
+
+* {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ font-weight: normal;
+ text-decoration: none;
+}
+
+a:link, [onclick] {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+html {
+ font-family: "Fira Sans", sans-serif;
+ font-size: 16px;
+ background-color: rgb(226, 192, 242);
+ padding: 0 1em;
+}
+
+body {
+ margin: 0 auto;
+ color: black;
+ background-color: white;
+ max-width: 56em;
+ padding: 0 1em;
+}
+
+header {
+ border-top: solid rgb(206, 132, 242) 0.5em;
+ border-bottom: solid rgb(244, 126, 126) 0.5em;
+ border-radius: 2.5em;
+ height: 6em;
+ transition: border-color 0.5s;
+ white-space: nowrap;
+ text-align: right;
+ margin: 0 auto;
+}
+
+header img {
+ float: left;
+ height: 100%;
+ width: auto;
+}
+
+header h1 {
+ font-size: 3em;
+ font-weight: normal;
+ margin-right: 0.25em;
+ display: inline-block;
+ height: 100%;
+ line-height: 2em;
+}
+
+header:hover {
+ border-top-color: rgb(244, 126, 126);
+ border-bottom-color: rgb(206, 132, 242);
+}
+
+img.logo {
+ -webkit-transition: -webkit-transform 1.5s;
+ transition: transform 1.5s;
+ -webkit-transition-delay: 0.5s;
+ transition-delay: 0.5s;
+ -webkit-transition-timing-function: cubic-bezier(0.4, 0.2, 0.5, 1.3);
+ transition-timing-function: cubic-bezier(0.4, 0.2, 0.5, 1.3);
+}
+
+img.logo:hover {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+}
+
+h2 {
+ border-top: solid rgb(206, 132, 242) 0.0625em;
+ border-left: solid rgb(206, 132, 242) 0.0625em;
+ border-radius: 1em 0 0 1em;
+ font-size: 1.25em;
+ font-weight: 300;
+ margin-left: -0.67em;
+ margin-right: -0.67em;
+ margin-top: 2em;
+ margin-bottom: 0.5em;
+ padding-left: 0.5em;
+ padding-top: 0.125em;
+}
+
+h3 {
+ border-top: solid rgb(206, 132, 242) 1px;
+ border-left: solid rgb(206, 132, 242) 1px;
+ border-radius: 1.5em;
+ display: inline-block;
+ font-size: 1.125em;
+ font-weight: 300;
+ margin-bottom: 0;
+ margin-left: -0.89em;
+ padding-left: 0.67em;
+ padding-right: 0.67em;
+ white-space: nowrap;
+}
+
+a:link, [onclick] {
+ color: rgb(206, 132, 242);
+ transition: color 0.3s;
+}
+
+a:visited {
+ color: rgb(206, 132, 242);
+}
+
+a:hover, [onclick]:hover {
+ color: rgb(244, 126, 126);
+}
+
+main {
+ display: block;
+ padding: 1em;
+ max-width: 50em;
+ margin: auto;
+}
+
+main > p {
+ margin: 1em 0.5em;
+}
+
+hr {
+ margin-top: 0;
+ margin-bottom: 1em;
+ border-bottom: solid rgb(206, 132, 242) 1px;
+ height: 2em;
+}
+
+.highlight {
+ border-bottom: solid rgb(206, 132, 242) 0.125em;
+ border-top: solid rgb(206, 132, 242) 0.125em;
+ border-radius: 1em;
+ font-weight: 300;
+ padding: 1em 3em;
+ transition: border-radius 0.3s;
+ box-sizing: border-box;
+}
+
+.highlight:hover {
+ border-radius: 3em;
+}
+
+.game-preview {
+ padding: 0;
+ text-align: center;
+ width: 100%;
+ max-width: 37em;
+ margin: auto;
+}
+
+.game-preview > li {
+ position: relative;
+ border: solid rgb(206, 132, 242) 1px;
+ margin-bottom: 1.5em;
+ margin-top: 1em;
+ margin-left: 2.5em;
+ border-left-width: 0;
+ height: 8em;
+ transition: border-radius 0.3s, border-color 0.3s;
+ padding-left: 4em;
+ box-sizing: border-box;
+ border-radius: 0 4em 4em 0;
+}
+
+.game-preview > li:nth-child(even) {
+ padding-left: 0;
+ padding-right: 4em;
+ margin-right: 2.5em;
+ margin-left: 0;
+ border-left-width: 1px;
+ border-right-width: 0;
+ border-radius: 4em 0 0 4em;
+}
+
+.game-preview > li:hover {
+ border-radius: 0 0.25em 0.25em 0;
+ border-color: rgb(244, 126, 126);
+}
+
+.game-preview > li img {
+ border: solid rgb(206, 132, 242) 1px;
+ position: absolute;
+ border-radius: 50%;
+ top: -1px;
+ left: -4em;
+ height: 8em;
+ width: 8em;
+ opacity: 0.75;
+ transition: border-radius 0.3s, border-color 0.3s, opacity 0.3s;
+ box-sizing: border-box;
+}
+
+.game-preview > li:nth-child(even) img {
+ left: auto;
+ right: -4em;
+}
+
+.game-preview > li:hover img {
+ border-radius: 0;
+ border-color: rgb(244, 126, 126);
+ opacity: 1.0;
+}
+
+.game-preview .info {
+ position: relative;
+ height: 100%;
+}
+
+.game-preview .info h4 {
+ color: inherit;
+ font-style: italic;
+ padding-left: 0.125em;
+ padding-right: 0.125em;
+ text-decoration: none;
+ text-align: center;
+ box-sizing: border-box;
+ padding-top: 0.25em;
+}
+
+.game-preview li:nth-child(odd) .info h4,
+.game-preview li:nth-child(odd) .info ul {
+ margin-right: 6.5rem;
+}
+
+.game-preview li:nth-child(even) .info h4,
+.game-preview li:nth-child(even) .info ul {
+ margin-right: 0em;
+ margin-left: 6.5rem;
+}
+
+h4 a:link {
+ text-decoration: none;
+}
+
+.game-preview .info p {
+ padding: 0.25em;
+ text-align: center;
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ -webkit-transform: translateY(-50%);
+ margin-top: 0.125em;
+}
+
+.game-preview .info ul {
+ bottom: 0.25em;
+ font-size: 0.75em;
+ position: absolute;
+ right: 0.125em;
+ left: 0.125em;
+ text-align: center;
+}
+
+.game-preview .info li:before {
+ content: ' ~ ';
+}
+
+.game-preview .info li:first-child:before {
+ content: '';
+}
+
+.game-preview .info li {
+ display: inline;
+ font-weight: 300;
+}
+
+ul.download {
+ border-radius: 1em;
+ border: solid rgb(206, 132, 242) 0.125em;
+ font-weight: 300;
+ padding: 0.5em;
+ margin: 1em auto;
+ transition: border-radius 0.3s, border-color 0.3s;
+ white-space: nowrap;
+ display: table;
+}
+
+ul.download:hover {
+ border-radius: 4px;
+ border-color: rgb(244, 126, 126);
+}
+
+ul.download li {
+ list-style-type: none;
+ font-size: 1.25em;
+ margin-bottom: 0.8em;
+ text-align: center;
+}
+
+ul.download li a {
+ text-decoration: none;
+}
+
+ul.download li.sh {
+ margin-bottom: 0;
+ font-size: 0.75em;
+ font-family: "Fira Mono", monospace;
+ text-align: left;
+}
+
+ul.download li:last-child {
+ margin-bottom: 0;
+}
+
+.sh:before {
+ content: "$ ";
+}
+
+input {
+ font-family: inherit;
+ font-size: 1em;
+}
+
+.copyright {
+ margin: auto;
+ text-align: justify;
+ width: 75%;
+ font-size: 0.875em;
+ font-weight: 400;
+}
+
+.copyright > p {
+ font-size: 0.875em;
+ font-weight: 300;
+}
+
+.copyright:before {
+ content: "Copyright ©";
+}
+
+pre {
+ border-left: solid rgba(206, 132, 242, 0.5) 8px;
+ border-radius: 8px;
+ font-family: "Fira Mono", monospace;
+ margin-left: 1em;
+ overflow: auto;
+ padding-left: 2em;
+ padding: 0.5em;
+ transition: background-color 0.3s, border-color 0.3s, border-radius 0.3s;
+}
+
+pre:hover {
+ background-color: rgba(206, 132, 242, 0.125);
+ border-color: rgb(206, 132, 242);
+ border-radius: 16px;
+}
+
+code {
+ font-family: "Fira Mono", monospace;
+ transition: background-color 0.3s;
+}
+
+code:hover {
+ background-color: rgba(206, 132, 242, 0.125);
+}
+
+pre code:hover {
+ background-color: transparent;
+}
+
+@media (max-width: 599px) {
+ html { font-size: 13px; }
+
+ .optional {
+ display: none;
+ }
+
+ ul.download li.sh:before {
+ content: "";
+ }
+
+ pre {
+ font-size: 0.875em;
+ }
+}
+
+@media (max-width: 479px) {
+ html { font-size: 10px; }
+}
+
--- /dev/null
+google-site-verification: google44dc8921b920c50f.html
\ No newline at end of file
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name=viewport content="width=device-width, initial-scale=1">
+ <link rel="stylesheet" href="css/main.css" type="text/css">
+ <title>Yukkuri Games</title>
+ <style>
+ body {
+ border-top: solid rgb(206, 132, 242) 0.75em;
+ border-bottom: solid rgb(244, 126, 126) 0.75em;
+ border-radius: 2em;
+ }
+
+ /* Reset usual header CSS */
+ header {
+ border: none;
+ height: 13em;
+ text-align: center;
+ }
+
+ header img {
+ float: none;
+ }
+
+ </style>
+ </head>
+
+ <body>
+ <header>
+ <img src="logotype.png" class=logo alt="Yukkuri Games">
+ </header>
+ <main>
+ <h2>Games</h2>
+ <ul class=game-preview>
+ <li>
+ <img src="thumbnails/pwl6.png" alt=Icon>
+ <div class=info>
+ <h4>Pixel Witch Lesson #6</h4>
+ <ul>
+ <li>Coming Summer 2014</li>
+ <li>Browser (WebGL), PC, Android</li>
+ </ul>
+ <p>
+ A puzzle game for witches in training.
+ </p>
+ </div>
+ </li>
+ </ul>
+ <h2>For Games</h2>
+ <ul class=game-preview>
+ <li>
+ <a href="/heroik/heroik.html">
+ <img src="/heroik/favicon_256.png" alt=Icon>
+ </a>
+ <div class=info>
+ <h4><a href="/heroik/heroik.html">Hero: Immortal King Scenario Generator</a></h4>
+ <p>
+ Random scenarios and
+ <a href="/heroik/variants.html">variant rules</a> for
+ Emmanuel Beltrando's dungeon-crawling card game
+ <em>Hero: Immortal King</em>.
+ </p>
+ <ul>
+ <li>2014</li>
+ <li>(Mobile) Browser</li>
+ </ul>
+ </div>
+ </li>
+
+ <li>
+ <a href="/mlpccg/mlpccg.html">
+ <img src="/mlpccg/favicon_256.png" alt=Icon>
+ </a>
+ <div class=info>
+ <h4><a href="/mlpccg/mlpccg.html">MLPCCG Helper</a></h4>
+ <p>
+ Play the <em>My Little Pony</em> <a href="http://enter-play.com/products/mlpccg.html">collectible card game</a>
+ even if, like us, you can't remember three numbers at
+ the same time.
+ </p>
+ <ul>
+ <li>2014</li>
+ <li>(Mobile) Browser</li>
+ </ul>
+ </div>
+ </li>
+
+ <li>
+ <a href="/labelle/">
+ <img src="/labelle/icon_128x128.png" alt=Icon>
+ </a>
+ <div class=info>
+ <h4><a href="/labelle/">Labelle Litanizer</a></h4>
+ <p>
+ Investigate the non-human with outfit-oriented ontology.
+ </p>
+ <ul>
+ <li>2013</li>
+ <li>(Mobile) Browser</li>
+ </ul>
+ </div>
+ </li>
+
+ <li>
+ <a href="http://psvzipper.appspot.com/">
+ <img src="thumbnails/psvzipper.png" alt=Icon>
+ </a>
+ <div class=info>
+ <h4><a href="http://psvzipper.appspot.com/">Vita Background Generator</a></h4>
+ <p>
+ Tetsuya Nomura Simulator, if his job was drawing
+ backgrounds for your PlayStation Vita.
+ </p>
+ <ul>
+ <li>2012</li>
+ <li>Browser</li>
+ </ul>
+ </div>
+ </li>
+ <li>
+ <a href="/enjoyable/">
+ <img src="thumbnails/enjoyable.png" alt=Icon>
+ </a>
+ <div class=info>
+ <h4><a href="/enjoyable/">Enjoyable</a></h4>
+ <p>
+ Map controller inputs to mouse and keyboard events. Many
+ of your games will become more fun.
+ </p>
+ <ul>
+ <li>2013</li>
+ <li>Mac OS X 10.7+</li>
+ </ul>
+ </div>
+ </li>
+ </ul>
+
+ <h2>Not Games (Yet?)</h2>
+ <ul class=game-preview>
+ <li>
+ <a href="/string-lerp/">
+ <img src="thumbnails/string-lerp.png" alt=Icon>
+ </a>
+ <div class=info>
+ <h4><a href="/string-lerp/">String Lerp</a></h4>
+ <p>
+ Animate one string progressively changing into another.
+ </p>
+ <ul>
+ <li>2014</li>
+ <li>JavaScript Module (Browser, Node)</li>
+ </ul>
+ </div>
+ </li>
+ <li>
+ <a href="/webcart1000/">
+ <img src="thumbnails/webcart1000.png" alt=Icon>
+ </a>
+ <div class=info>
+ <h4><a href="/webcart1000/">WEBCART1000</a></h4>
+ <p>
+ Share save files between multiple browser-based games.
+ </p>
+ <ul>
+ <li>2013</li>
+ <li>JavaScript Module (Browser)</li>
+ </ul>
+ </div>
+ </li>
+ <li>
+ <a href="https://code.google.com/p/python-bulletml">
+ <img src='thumbnails/bulletml.png' alt=Icon>
+ </a>
+ <div class=info>
+ <h4>
+ <a href="https://code.google.com/p/python-bulletml">
+ python-bulletml
+ </a>
+ </h4>
+ <p>
+ Parse and run BulletML scripts in Python.
+ </p>
+ <ul>
+ <li>2010</li>
+ <li>Python Module (2.6+, 3.x)</li>
+ </ul>
+ </div>
+ </li>
+ </ul>
+ <h2>Game Jams</h2>
+ <ul class=game-preview>
+ <li>
+ <a href="/123456789/">
+ <img src="/thumbnails/123456789.png" alt=Icon>
+ </a>
+ <div class=info>
+ <h4><a href="/123456789/">123456789</a></h4>
+ <p>
+ A puzzle-<wbr>paean
+ from <a href="http://www.gamesetwatch.com/2007/03/column_beyond_tetris_nemesis_f_1.php">one
+ forgotten input chimera</a> to another near extinction.
+ </p>
+ <ul>
+ <li>2013</li>
+ <li>LD26, "minimalism"</li>
+ <li>Browser</li>
+ </ul>
+ </div>
+ </li>
+ <li>
+ <a href="/pphs/">
+ <img src="/thumbnails/rogue1980.png" alt=Icon>
+ </a>
+ <div class=info>
+ <h4><a href="/pphs/">Rogue (1980)</a></h4>
+ <p>
+ Speculative digital archaeology. Also, a poorly-balanced
+ dungeon-crawler-<em>cum</em>-flirting-minigame.
+ </p>
+ <ul>
+ <li>2013</li>
+ <li><a href="http://www.electricopolis.net/2013/03/29/pphs-jam/">♥♡♥PPHS♥♡♥</a></li>
+ <li>Version 7 Unix, 4.2BSD, IBM PC</li>
+ </ul>
+ </div>
+ </li>
+
+ <li>
+ <a href="/matrixcreatrix/">
+ <img src="/thumbnails/matrixcreatrix.png" alt=Icon>
+ </a>
+ <div class=info>
+ <h4><a href="/matrixcreatrix/">Matrix Creatrix</a></h4>
+ <p>
+ Whatever the opposite of color-matching games are, while
+ still being a color-matching game.
+ </p>
+ <ul>
+ <li>2012</li>
+ <li>LD23, "tiny world"</li>
+ <li>Browser (Perlenspiel)</li>
+ </ul>
+ </div>
+ </li>
+
+ <li>
+ <a href="http://pyweek.org/e/kuri/">
+ <img src="/thumbnails/featherfall.png" alt=Icon>
+ </a>
+ <div class=info>
+ <h4><a href="http://pyweek.org/e/kuri/">Feather Fall</a></h4>
+ <p>
+ High-flying fast-falling arcade action.
+ </p>
+ <ul>
+ <li>2009</li>
+ <li>PyWeek #9, "feather"</li>
+ <li>GNU/Linux, Windows</li>
+ </ul>
+ </div>
+ </li>
+ <li>
+ <a href="http://pyweek.org/e/Yukkuri/">
+ <img src="/thumbnails/lagomorph.png" alt=Icon>
+ </a>
+ <div class=info>
+ <h4><a href="http://pyweek.org/e/Yukkuri/">I Am Lagomorph</a></h4>
+ <p>
+ Tend a farm threatened by undead bunnies and
+ poop. Unfinished and unstable.
+ </p>
+ <ul>
+ <li>2009</li>
+ <li>PyWeek #8, "get off my lawn"</li>
+ <li>? (Python, OpenGL)</li>
+ </ul>
+ </div>
+ </li>
+ </ul>
+ </main>
+ </body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name=viewport content="width=device-width, initial-scale=1">
+ <link rel="stylesheet" href="/css/main.css" type="text/css">
+ <title>String Lerp - Yukkuri Games</title>
+ <style>
+ input[type=text] {
+ margin: 0.125em 1em;
+ width: 40%;
+ text-align: center;
+ }
+ </style>
+ <script type="text/javascript" src="string-lerp.js"></script>
+ <script>
+ var DEMOS = [
+ ["explore", "implode"],
+ ["Do you like green eggs and ham?", "I do not like them, Sam-I-am."],
+ ["apple core", "core dump"],
+ ["rgb(255, 0, 0)", "rgb(0, 128, 255)"],
+ ["chicken wing", "buffalo wing"],
+ ["1.5 + 1.5 ≈ 3", "3.0 + 7.0 ≈ 10"],
+ ["<(o.o<) v(._.)v", "(>o.o)> ^(*_*)^"],
+ ["ZALGO̸", "ZA̢LG͜O"],
+ ["", "Typing, one letter at a time."],
+ ["( ノ゚▽゚)ノ", "( ╯︵╰)"]
+ ];
+ var i = Math.floor(Math.random() * DEMOS.length);
+
+ function set () {
+ var a = document.getElementById("a");
+ var b = document.getElementById("b");
+ var d = DEMOS[i++ % DEMOS.length];
+ a.value = d[0];
+ b.value = d[1];
+ update();
+ }
+
+ function escape (s) {
+ return s.replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/ /g, " ");
+
+ }
+
+ function update () {
+ var p = +document.getElementById("slider").value;
+ var a = document.getElementById("a");
+ var b = document.getElementById("b");
+ var e = document.getElementById("text");
+ e.innerHTML = escape(stringLerp.lerp(a.value, b.value, p));
+ }
+ </script>
+ </head>
+ <body onload="set()">
+ <header>
+ <a href="/">
+ <img src="/logotype_horizontal_1.png" class=logo alt="(◕ ヮ ◕)">
+ <img src="/logotype_horizontal_2.png" class=optional
+ alt="Yukkuri Games">
+ </a>
+ <h1>String Lerp</h1>
+ </header>
+ <main>
+ <p>
+ String Lerp is a JavaScript module to interpolate (lerp,
+ blend, tween) between two string values, that is,
+ progressively turn one string into another. It
+ uses <a href="http://en.wikipedia.org/wiki/Levenshtein_distance">Levenshtein
+ distance</a> measurements along with other heuristics to try
+ to blend between two strings as naturally as possible.
+ </p>
+ <ul class=download>
+ <li><a href="string-lerp-1.0.0.tar.gz">string-lerp-1.0.0.tar.gz</a> (16KB)
+ <li class=sh>npm install <a href="https://www.npmjs.org/package/string-lerp">string-lerp</a>
+ <li class=sh><span data-optional>git clone
+ </span><a href="http://git.yukkurigames.com/string-lerp.git">http://git.yukkurigames.com/string-lerp.git</a>
+ </ul>
+ <p style="text-align: center">
+ Not really sure what that means? Try it out.
+ </p>
+ <div class=highlight>
+ <div style="width: 100%; text-align: center">
+ <input type="text" id="a" value="Do you like green eggs and ham?"
+ oninput="update();">
+ <input type="text" id="b" value="I do not like them, Sam-I-am."
+ oninput="update();">
+ </div>
+ <input id=slider type=range value=0.5 max=1.00 step=0.00390625
+ oninput="update();"
+ style="width: 80%; margin: 1em auto; display: block;">
+ <div id=text style="font-size: 1.5em; text-align: center; min-height: 1.5em;">
+ </div>
+ <div style="text-align: center">
+ <a onclick="set();">Try a different string pair</a>
+ </div>
+ </div>
+ <h2>API</h2>
+ <p>In a browser, use</p>
+ <pre><code><script type="text/javascript" src="string-lerp.js"></script></code></pre>
+ <p>In Node.js and other non-browser environments,</p>
+ <pre><code>var stringLerp = require("./string-lerp")</code></pre>
+
+ <p>Then,</p>
+ <pre><code>var result = stringLerp.lerp(source, target, amount);
+ // `source' is the string to start with
+ // `target' is the string to finish with
+ // `amount' is an amount to edit the strings, between 0 and 1,
+ // e.g. 0.23 = 23% from source to target</code></pre>
+ <p>
+ The internal <code>diff</code> and <code>patch</code> routines
+ are also exposed, as <code>diffLerp</code> is too slow for
+ very long strings unless you compute and store the diff list
+ ahead of time.
+ </p>
+ <h2>License</h2>
+ <div class=copyright>
+ 2014 Joe Wreschnig
+ <p>
+ String Lerp is free software. You can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation;
+ either <a href="http://www.gnu.org/licenses/gpl-2.0.html">version
+ 2 of the License</a>, or (at your
+ option) <a href="http://www.gnu.org/licenses/gpl.html">any
+ later version</a>.
+ </p>
+ </div>
+ </main>
+ </body>
+</html>
--- /dev/null
+/* string-lerp - progressively turn one string into another
+ Copyright 2014 Joe Wreschnig
+ Licensed under the terms of the GNU GPL v2 or later
+ @license http://www.gnu.org/licenses/gpl-2.0.html
+ @source: http://yukkurigames.com/string-lerp/
+*//*
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 2 of the
+ License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ As additional permission, you may distribute the program or works
+ based on it without the copy of the GNU GPL normally required,
+ provided you include this license notice and a URL through which
+ recipients can access the corresponding source code.
+*/
+
+/*globals exports, Uint32Array */
+
+(function (exports) {
+ "use strict";
+
+ var MAX_MATRIX_SIZE = 256 * 256;
+
+ function costMatrix(source, target, ins, del, sub) {
+ /** Calculate the Levenshtein cost matrix for source and target
+
+ If source and target are strings, they cannot contain any
+ astral or combining codepoints. Such data must be passed
+ as arrays of strings with one element per glyph.
+
+ ins, del, and sub are the costs for insertion, deletion,
+ and substition respectively. Their default value is 1. If
+ only ins is passed, del and sub are set to the same cost.
+ If ins and del are passed, sub is set to the more
+ expensive of the two.
+
+ The matrix is returned as a flat typed array.
+
+ Following http://en.wikipedia.org/wiki/Levenshtein_distance
+ */
+ ins = ins === undefined ? 1 : (ins | 0);
+ del = (del | 0) || ins;
+ sub = (sub | 0) || Math.max(ins, del);
+ var m = source.length + 1;
+ var n = target.length + 1;
+ var d = new Uint32Array(m * n);
+ var i, j;
+ for (i = 1; i < m; ++i)
+ d[n * i] = i;
+ for (j = 1; j < n; ++j)
+ d[j] = j;
+ for (j = 1; j < n; ++j)
+ for (i = 1; i < m; ++i)
+ if (source[i - 1] === target[j - 1])
+ d[n * i + j] = d[n * (i - 1) + j - 1];
+ else
+ d[n * i + j] = Math.min(del + d[n * (i - 1) + j ],
+ ins + d[n * i + j - 1],
+ sub + d[n * (i - 1) + j - 1]);
+ return d;
+ }
+
+ // First, note that deletion is just substition with nothing, so
+ // any DEL operation can be replaced by a SUB. Second, the
+ // operation code *is* the necessary slice offset for applying the
+ // diff.
+ var INS = 0, SUB = 1;
+
+ function editPath(costs, target) {
+ /** Given a cost matrix and a target, create an edit list */
+ var path = [];
+ var j = target.length;
+ var n = j + 1;
+ var i = costs.length / n - 1;
+ while (i || j) {
+ var sub = (i && j) ? costs[n * (i - 1) + j - 1] : Infinity;
+ var del = i ? costs[n * (i - 1) + j] : Infinity;
+ var ins = j ? costs[n * i + j - 1] : Infinity;
+ if (sub <= ins && sub <= del) {
+ if (costs[n * i + j] !== costs[n * (i - 1) + j - 1])
+ path.push([SUB, i - 1, target[j - 1]]);
+ --i; --j;
+ } else if (ins <= del) {
+ path.push([INS, i, target[j - 1]]);
+ --j;
+ } else {
+ path.push([SUB, i - 1, ""]);
+ --i;
+ }
+ }
+ return path;
+ }
+
+ function diff(source, target, ins, del, sub) {
+ /** Create a list of edits to turn source into target
+
+ ins, del, and sub are as passed to costMatrix.
+ */
+ return editPath(costMatrix(source, target, ins, del, sub), target);
+ }
+
+ function patchArray(diff, source) {
+ for (var i = 0; i < diff.length; ++i) {
+ var edit = diff[i];
+ source.splice(edit[1], edit[0], edit[2]);
+ }
+ return source;
+ }
+
+ function patchString(diff, source) {
+ for (var i = 0; i < diff.length; ++i) {
+ var edit = diff[i];
+ var head = source.slice(0, edit[1]);
+ var tail = source.slice(edit[1] + edit[0]);
+ source = head + edit[2] + tail;
+ }
+ return source;
+ }
+
+ function patch(diff, source) {
+ /** Apply a list of edits to source */
+ var patcher = Array.isArray(source) ? patchArray : patchString;
+ return patcher(diff, source);
+ }
+
+ // Matches if a string contains combining characters or astral
+ // codepoints (technically, the first half surrogate of an astral
+ // codepoint).
+ var MULTI = /[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uD800-\uDBFF\uFE20-\uFE2F]/;
+
+ // Match an entire (potentially astral) codepoint and any
+ // combining characters following it.
+ var GLYPH = /[\0-\u02FF\u0370-\u1DBF\u1E00-\u20CF\u2100-\uD7FF\uD800-\uFE1F\uFE30-\uFFFF][\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uDC00-\uDFFF\uFE20-\uFE2F]*/g;
+
+ function diffLerpAstral(source, target, amount) {
+ // This split is not perfect for all languages, but at least
+ // it won't create invalid surrogate pairs or orphaned
+ // combining characters.
+ var sourceGlyphs = source.match(GLYPH) || [];
+ var targetGlyphs = target.match(GLYPH) || [];
+ var edits = diff(targetGlyphs, sourceGlyphs, 2, 2, 3);
+ // The edit path works from the string end, forwards, because
+ // that's how Levenshtein edits work. To match LTR reading
+ // direction (and the behavior of fastLerp), swap the strings
+ // and invert the parameter when editing.
+ var partial = edits.slice(0, Math.round((1 - amount) * edits.length));
+ return patchArray(partial, targetGlyphs).join("");
+ }
+
+ function diffLerpBasic(source, target, amount) {
+ var edits = diff(target, source, 2, 2, 3);
+ // The edit path works from the string end, forwards, because
+ // that's how Levenshtein edits work. To match LTR reading
+ // direction (and the behavior of fastLerp), swap the strings
+ // and invert the parameter when editing.
+ var partial = edits.slice(0, Math.round((1 - amount) * edits.length));
+ return patchString(partial, target);
+ }
+
+ function diffLerp(source, target, amount) {
+ /** Interpolate between two strings using edit operations
+
+ This interpolation algorithm applys a partial edit of one
+ string into the other. This produces nice looking results,
+ but can take a significant amount of time and memory to
+ compute the edits. It is not recommended for strings
+ longer than a few hundred characters.
+ */
+
+ if (source.match(MULTI) || target.match(MULTI))
+ return diffLerpAstral(source, target, amount);
+ else
+ return diffLerpBasic(source, target, amount);
+ }
+
+ var NUMBERS = /(-?\d{1,20}(?:\.\d{1,20})?)/g;
+
+ function areNumericTwins(source, target) {
+ /** Check if a and b differ only in numerals */
+ return source.replace(NUMBERS, "0") === target.replace(NUMBERS, "0");
+ }
+
+ function nlerp(source, target, amount) {
+ return source + (target - source) * amount;
+ }
+
+ function numericLerp(source, target, amount) {
+ /** Interpolate numerically between strings containing numbers
+
+ Numbers may have a leading "-" and a single "." to mark
+ the decimal point, but something must be after the ".".
+ No other floating point syntax (e.g. 1e6) is supported.
+ They are treated as fixed-point values, with the point's
+ position itself interpolating.
+
+ For example, numericLerp("0.0", "100".0, 0.123) === "12.3"
+ because the "." in "0.0" is interpreted as a decimal
+ point. But numericLerp("0.", "100.", 0.123) === "12."
+ because the strings are interpreted as integers followed
+ by a full stop.
+
+ Calling this functions on strings that differ in more than
+ numerals gives undefined results.
+ */
+
+ var targetParts = target.split(NUMBERS);
+ var match;
+ var i = 1;
+ while ((match = NUMBERS.exec(source))) {
+ var sourcePart = match[0];
+ var targetPart = targetParts[i];
+ var part = nlerp(+sourcePart, +targetPart, amount);
+ var sourcePoint = sourcePart.indexOf(".");
+ var targetPoint = targetPart.indexOf(".");
+ var point = Math.round(nlerp(
+ sourcePoint >= 0 ? (sourcePart.length - 1) - sourcePoint : 0,
+ targetPoint >= 0 ? (targetPart.length - 1) - targetPoint : 0,
+ amount));
+ targetParts[i] = part.toFixed(point);
+ i += 2;
+ }
+ return targetParts.join("");
+ }
+
+ function fastLerpAstral(source, target, amount) {
+ var sourceGlyphs = source.match(GLYPH) || [];
+ var targetGlyphs = target.match(GLYPH) || [];
+ var sourceLength = Math.round(sourceGlyphs.length * amount);
+ var targetLength = Math.round(targetGlyphs.length * amount);
+ var head = targetGlyphs.slice(0, targetLength);
+ var tail = sourceGlyphs.slice(sourceLength, sourceGlyphs.length);
+ head.push.apply(head, tail);
+ return head.join("");
+ }
+
+ function fastLerpBasic(source, target, amount) {
+ var sourceLength = Math.round(source.length * amount);
+ var targetLength = Math.round(target.length * amount);
+ var head = target.substring(0, targetLength);
+ var tail = source.substring(sourceLength, source.length);
+ return head + tail;
+ }
+
+ function fastLerp(source, target, amount) {
+ /** Interpolate between two strings based on length
+
+ This interpolation algorithm progressively replaces the
+ front of one string with another. This approach is fast
+ but does not look good when the strings are similar.
+ */
+
+ // TODO: Consider fast-pathing this even more for very large
+ // strings, e.g. in the megabyte range. These are large enough
+ // that it should be fine to just pick a codepoint and search
+ // for the nearest glyph start.
+ if (source.match(MULTI) || target.match(MULTI))
+ return fastLerpAstral(source, target, amount);
+ else
+ return fastLerpBasic(source, target, amount);
+ }
+
+ function lerp(source, target, amount) {
+ /** Interpolate between two strings as best as possible
+
+ If the strings are identical aside from numbers in them,
+ they are passed through numericLerp.
+
+ If the strings are not numbers and short, they are passed
+ through diffLerp.
+
+ Otherwise, they are passed through fastLerp.
+ */
+ source = source.toString();
+ target = target.toString();
+
+ // Fast path for boundary cases.
+ if (amount === 0) return source;
+ if (amount === 1) return target;
+
+ if (areNumericTwins(source, target))
+ return numericLerp(source, target, amount);
+
+ // Numeric lerps should over- and under-shoot when fed numbers
+ // outside 0 to 1, but other types cannot.
+ if (amount < 0) return source;
+ if (amount > 1) return target;
+
+ var n = source.length * target.length;
+ var appropriate = (n && n < MAX_MATRIX_SIZE) ? diffLerp : fastLerp;
+ return appropriate(source, target, amount);
+ }
+
+ exports.costMatrix = costMatrix;
+ exports.patch = patch;
+ exports.diff = diff;
+ exports.fastLerp = fastLerp;
+ exports.diffLerp = diffLerp;
+ exports.numericLerp = numericLerp;
+ exports.lerp = lerp;
+
+})(typeof exports === "undefined" ? (this.stringLerp = {}) : exports);