Commit 10f4aa7c authored by Leon Arnott's avatar Leon Arnott
Browse files

Added (stripes:), a variant of (gradient:) which creates a striped background...

Added (stripes:), a variant of (gradient:) which creates a striped background (for use with (background:)) when given a given stripe width, angle, and colours.

As of now, the adjusted "stops" data value on (stripes:)'s gradients may seem ugly, but it leaves the possibility open for someday adding (repeating-gradient:), which would use the same {pixels,colour} names.

Also fixed a long-standing bug where, due to a precedence conflict, writing something like `$a's stops's (1)` would cause a Javascript error.
parent 30e505503f6a
......@@ -10,6 +10,7 @@ Documentation is at http://twine2.neocities.org/. See below for compilation inst
* Fixed a long-standing bug involving the interaction between temp variables and the `(show:)` and `(link-show:)` macros. The previous behaviour, which was not my intention at all, meant that hooks shown with `(show:)` could only access temp variables available at the `(show:)` macro's location, rather than the hook's location (which is now the current behaviour). The same applies to `(link-show:)`, as well as to the new `(rerun:)` macro (below).
* Fixed a long-standing bug where continuous ranges for arrays, such as `(a: 1,2)'s 4thlasttolast`, wouldn't work correctly. (What that example should do is provide the entire array, as is consistent with Python.)
* Fixed a long-standing bug where `(click: ?Passage)` and `(click: ?Sidebar)` just flat-out didn't work at all.
* Fixed a long-standing bug where, due to an operator precedence conflict, writing something like `$a's stops's (1)` would cause a Javascript error.
* Fixed a long-standing bug where lambdas would produce an incorrect duplicate-name error if the temp variables used with their clauses contained capital letters.
* Fixed a long-standing bug where hooks that had `(transition:)` transitions would restart their transition animations whenever the containing passage finished transitioning in. Previously, the only way to overcome this was to make the passage transition using `(transition-arrive:"instant")`.
* Fixed a long-standing bug where you couldn't use the column markup to create empty columns by placing two column markup lines in succession, without an intervening blank line.
......@@ -129,6 +130,7 @@ Documentation is at http://twine2.neocities.org/. See below for compilation inst
* Added an `(lch:)` macro (alias `(lcha:)`), an alternative colour creation macro (comparable to `(hsl:)`) that uses the LCH colour space (which is the CIELAB colour space, familiar to graphic designers, but expressed radially using a HSL-style hue angle).
* Added a `(complement:)` macro, which takes a colour and produces its complement by rotating its LCH hue by 180 degrees.
* Added a `(palette:)` macro, designed for rapid prototyping, which produces a four-colour palette based on a given colour, for use with `(text-colour:)`, `(background:)` and `(enchant:)`.
* Added `(stripes:)`, a variant of `(gradient:)` which creates a striped background (for use with `(background:)`) when given a given stripe width, angle, and colours.
###### Values
......
......@@ -15,7 +15,7 @@ define(['utils/operationutils'], ({toSource}) => {
| Data name | Example | Meaning
|---
| `angle` | `$grad's angle` | The angle, a number from 0 to 359. If a value outside this range was given to `(gradient:)`, this will return that value wrapped to this range.
| `stops` | `$grad's stops` | An array of the gradient's colour-stops. Each stop is a datamap containing "percent" and "colour" data names. <br>`(gradient: 90, 0.2, blue, 0.8, white)'s stops` is `(a:(dm: "percent", 0.2, "colour", blue), (dm: "percent", 0.8, "colour", white))`.
| `stops` | `$grad's stops` | An array of the gradient's colour-stops, represented as datamaps. Gradients created by (gradient:) have "percent" and "colour" data names in this datamap. For (stripes:) gradients, "pixels" and "colour" data names appear instead. <br>`(gradient: 90, 0.2, blue, 0.8, white)'s stops` is `(a:(dm: "percent", 0.2, "colour", blue), (dm: "percent", 0.8, "colour", white))`.
Gradients, when used in passage prose or given to (print:), produce a square swatch containing the gradient. This is a `<tw-colour>`
element, but otherwise has no other features or capabilities and is intended solely for debugging purposes.
......@@ -45,7 +45,7 @@ define(['utils/operationutils'], ({toSource}) => {
a deep property of a gradient, such as (set: $grad's stops's (0.1) to blue),
even though this does nothing.
*/
return this.stops.map(stop => new Map([["percent",stop.stop], ["colour", stop.colour.TwineScript_Clone()]]));
return this.stops.map(stop => new Map([[this.repeating ? "pixels" : "percent", stop.stop], ["colour", stop.colour.TwineScript_Clone()]]));
}
},
TwineScript_Properties: ["angle","stops"],
......@@ -72,9 +72,9 @@ define(['utils/operationutils'], ({toSource}) => {
return "<tw-colour style='background:" + this.toLinearGradientString() + "'></tw-colour>";
},
// This accepts an integer degree, plus any number of [percent, colour] pairs.
create(angle, stops) {
return Object.assign(Object.create(this), { angle, stops: stops.sort((a,b) => a.stop - b.stop) });
// This accepts an integer degree, plus any number of [distance, colour] pairs.
create(angle, stops, repeating = false) {
return Object.assign(Object.create(this), { angle, stops: stops.sort((a,b) => a.stop - b.stop), repeating });
},
/*
......@@ -88,9 +88,9 @@ define(['utils/operationutils'], ({toSource}) => {
This converts the colour into a CSS linear-gradient() function.
*/
toLinearGradientString() {
return `linear-gradient(${this.angle}deg, ${
return (this.repeating ? 'repeating-' : '') + `linear-gradient(${this.angle}deg, ${
this.stops.reduce((str, {colour,stop}) =>
str + colour.toRGBAString() + " " + (stop*100) + "%,",
str + colour.toRGBAString() + " " + (stop*(this.repeating ? 1 : 100)) + (this.repeating ? "px," : "%,"),
'').slice(0,-1)
})`;
},
......
......@@ -1025,9 +1025,9 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
The second, optional string is a label that is placed inside the meter, on top of the bar. This text is aligned in the same direction as the meter itself.
Either a colour or a gradient can be given as the final value, with which to colour the bar. If neither is given, the bar will be a simple gray.
If a gradient is given, its rotation will be automatically changed to 90 degrees, with the leftmost colour (at colour stop 0) being placed at
the "empty" end of the meter, and the rightmost colour (at colour stop 1) placed at the "full" end. If the bar is center-aligned, then
the gradient will be modified, with both ends of the graph having the leftmost colour, and the center having the rightmost colour.
If a gradient is given, and it isn't a (stripes:) gradient, its rotation will be automatically changed to 90 degrees, with the leftmost colour
(at colour stop 0) being placed at the "empty" end of the meter, and the rightmost colour (at colour stop 1) placed at the "full" end. If the bar
is center-aligned, then the gradient will be modified, with both ends of the graph having the leftmost colour, and the center having the rightmost colour.
The meter is exclusively horizontal, and cannot be rotated unless you attach (text-rotate:) to it.
......@@ -1093,18 +1093,23 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
*/
const makeStyleString = value => {
const clampedSize = max(0, min(1, value / maxValue));
const g = gradient.multiply(maxValue / value);
/*
The method for cutting a non-repeating gradient down by a percentage amount is to think
of the problem from the opposite side: simply multiply the gradient's colour stops by
the percentage's reciprocal, so that they extend beyond 100%.
*/
const g = gradient.repeating ? gradient : gradient.multiply(maxValue / value);
return `height:100%;background-repeat:no-repeat;background-image:${
/*
A center-aligned graph consists of two different gradient backgrounds, extending to the left and right
from the centre. This line produces the left extension.
*/
(isCenter ? assign(g, { angle: 270 }).toLinearGradientString() + ", " : '')
(isCenter ? assign(g, g.repeating ? {} : { angle: 270 }).toLinearGradientString() + ", " : '')
/*
And, this line produces either the right extension, or the entire bar for other alignments.
Note that the gradient angle is reversed for right-alignment (270 rather than 90).
*/
+ assign(g, { angle: isCenter || marginLeft === 0 ? 90 : 270 }).toLinearGradientString()
+ assign(g, g.repeating ? {} : { angle: isCenter || marginLeft === 0 ? 90 : 270 }).toLinearGradientString()
};background-size:${
isCenter ? Array(2).fill(clampedSize * 50 + "%") : clampedSize * 100 + "%"
};background-position-x:${
......
......@@ -59,8 +59,9 @@ define(['jquery', 'utils', 'utils/operationutils', 'engine', 'state', 'passages'
Details:
The (change:) macro can target plain text instead of hooks, much like (click:) - simply provide a string instead of a hook name.
If a "via" lambda is supplied to (change:) instead of a changer, then that lambda is used to compute a changer dynamically, based on specifics of
each hook that's enchanted. For instance, `(change: "O", via (text-style:(cond: pos % 2 is 0, 'bold', 'none')))`
If a "via" lambda is supplied to (change:) instead of a changer, then that lambda is used to compute a changer dynamically, using the `pos` keyword
to distinguish each hook that's enchanted. For instance, `(change: "O", via (text-style:(cond: pos is an even, 'bold', 'none')))` changes only
even-numbered instances of the letter "O".
Like the (replace:), (append:) and (prepend:) macros, this macro does not affect text and hooks that appear after it, as it is an immediate
command that only affects what has already been rendered. For an alternative version of this macro which does affect hooks and text after it,
......
......@@ -1677,7 +1677,7 @@ define(['jquery','macros', 'utils', 'utils/renderutils', 'datatypes/colour', 'da
if (Colour.isHexString(value) || Colour.isCSS3Function(value)) {
property = {"background-color": value};
}
else if (value.startsWith('linear-gradient(')) {
else if (value.startsWith('linear-gradient(') || value.startsWith('repeating-linear-gradient(')) {
property = {"background-image": value};
}
else {
......
......@@ -7,7 +7,7 @@ define(['macros', 'utils', 'utils/operationutils', 'datatypes/colour', 'datatype
*/
const
{rest, zeroOrMore, either, optional, insensitiveSet, numberRange, percent, nonNegativeInteger, Any } = Macros.TypeSignature,
{rest, zeroOrMore, either, optional, insensitiveSet, numberRange, percent, nonNegativeInteger, positiveInteger, Any } = Macros.TypeSignature,
{max,min,round,floor,ceil} = Math;
Macros.add
......@@ -984,10 +984,13 @@ define(['macros', 'utils', 'utils/operationutils', 'datatypes/colour', 'datatype
This is to help ensure that the initial degree number is not confused for a colour-stop percentage. Additionally, CSS
linear-gradient "colour hints", which are used to adjust the midpoints between colour stops, are currently not supported by this macro.
See also:
(stripes:)
Added in: 3.1.0
#colour 6
*/
(["gradient"], (_, degree, ...args) => {
("gradient", (_, degree, ...args) => {
/*
Just like with (hsl:), we silently rounded and truncate degrees to the 0..359 range.
*/
......@@ -1052,6 +1055,65 @@ define(['macros', 'utils', 'utils/operationutils', 'datatypes/colour', 'datatype
},
[Number, rest(either(percent, Colour))])
/*d:
(stripes: Number, Number, Colour, Colour) -> Gradient
When given a degree angle, a pixel distance, and two or more colours, this macro produces
a gradient that draws a striped background, with each stripe as wide as the distance, and alternating through the given colours.
Example usage:
* `(enchant: ?page, (background: (gradient: 45, 20, fuchsia+white, white)))` causes the page to have a pink-and-white striped background.
Rationale:
The (gradient:) macro can be used to dynamically create gradient backgrounds, which smoothly transition between multiple colours. By using
certain pairs of colour stops that are very close together, however, you can create gradients where the colours transition sharply, producing stripes.
Rather than use that macro, you can instead use this one to generate striped backgrounds succinctly, that repeat uniformly, and with easily-adjusted
stripe width.
Details:
The degree angle matches that given to (gradient:). A number of 0 causes the stripes to be drawn horizontally, and increasing that number rotates
the stripes counter-clockwise. Any number below 0 or above 359 will automatically be rounded to fit the 0-359
degree range - so, a value of 380 will become 20.
The distance value given is in pixels, and determines the width of a single stripe.
Gradients (including those produced by (stripes:)) in Harlowe are implemented using CSS `repeating-linear-gradient`s, and have the same limitations
in output and browser support.
(stripes:) gradients still have a "stops" array, accessible via `$stripeGradient's stops`, as with other gradients. Even though (stripes:) doesn't accept
"colour stops", this array still contains colour stop datamaps, as if this gradient had been generated by (gradient:).
There are two "stops" for each colour, and instead of a "percent" value, they have a "pixels" value. So, `$stripeGradient's stops's 1st's colour` will
produce the colour of the first stripe, `$stripeGradient's stops's 3rd's colour` will produce the colour of the second stripe, and so forth.
`$stripeGradient's stops's 2nd's pixels` will produce the pixel width of each stripe.
See also:
(gradient:)
Added in: 3.2.0
#colour 7
*/
("stripes", (_, degree, width, ...colours) => {
/*
Just like with (hsl:), we silently rounded and truncate degrees to the 0..359 range.
*/
degree = round(degree) % 360;
if (degree < 0) {
degree += 360;
}
/*
Stripes are produced in a much simpler fashion from (gradient:).
*/
let stop = 0;
const pairs = [];
colours.forEach(c => {
pairs.push({ stop, colour:clone(c) });
stop += width;
pairs.push({ stop, colour:clone(c) });
});
return Gradient.create(degree, pairs, true);
},
[Number, positiveInteger, Colour, rest(Colour)])
/*d:
(cond: Boolean, Any, ...Any) -> Any
......
......@@ -162,12 +162,8 @@ define(['utils'], ({impossible}) => {
created by converting "addition" and "subtraction" in compile().
*/
{rightAssociative: ["not", "positive", "negative"]},
{rightAssociative: ["belongingProperty"]},
{rightAssociative: ["belongingOperator", "belongingItOperator"]},
["property"],
["itsProperty"],
{rightAssociative: ["belongingItProperty"]},
["possessiveOperator", "itsOperator"],
{rightAssociative: ["belongingProperty","belongingItProperty", "belongingOperator", "belongingItOperator"]},
["property","itsProperty", "possessiveOperator", "itsOperator"],
["twineLink"],
["macro"],
["grouping"],
......@@ -868,7 +864,7 @@ define(['utils'], ({impossible}) => {
/*
Otherwise, create the error object for end-user examination.
*/
return "TwineError" + dotCreate + "'operation','I need some code to be "
return "TwineError" + dotCreate + "'operation','I need usable code to be "
+ (needsLeft === MUST ? "left " : "")
+ (needsLeft === MUST && needsRight === MUST ? "and " : "")
+ (needsRight === MUST ? "right " : "")
......
......@@ -673,6 +673,13 @@ describe("style changer macros", function() {
done();
});
});
it("given a (stripes:) gradient, applies it as the background-image property", function(done) {
var p = runPassage("(background:(stripes:45,32,#00FF00,#000))[Hey]").find('tw-hook');
setTimeout(function() {
expect(p).toHaveBackgroundStripes(45, 32, ["#00FF00", "#000000"]);
done();
});
});
it("can compose with itself", function(done) {
var p = runPassage("(set: $x to (background:'#800000')+(background:'garply'))$x[Hey]").find('tw-hook');
setTimeout(function() {
......
......@@ -587,7 +587,7 @@ describe("interface macros", function(){
p = runPassage("(set:$foo to 10)(meter: bind $foo, 200, '=X', 'Yo')");
expect(p.find('tw-meter').css('background-size')).toBe('5%');
});
it("uses the given gradient or colour for the bar, ignoring the angle", function() {
it("uses the given gradient or colour for the bar, ignoring the angle if it isn't a (stripes:) gradient", function() {
var p = runPassage("(set:$foo to 100)(meter: bind $foo, 100, '=X', (gradient:45,0,#ab1212,1,#ac6060))");
expect(p.find('tw-meter')).toHaveBackgroundGradient(270, [
{stop:0,colour:"#AB1212"},
......@@ -603,13 +603,17 @@ describe("interface macros", function(){
{stop:0,colour:"#FADABC"},
{stop:1,colour:"#FADABC"},
]);
p = runPassage("(set:$foo to 100)(meter: bind $foo, 100, 'X=', (stripes:125,14,#22FF00,#A00))");
expect(p.find('tw-meter')).toHaveBackgroundStripes(125, 14, ["#22FF00", "#AA0000"]);
});
it("scales the gradient based on how much of the bar is remaining", function() {
it("if it isn't a (stripes:) gradient, scales the gradient based on how much of the bar is remaining", function() {
var p = runPassage("(set:$foo to 50)(meter: bind $foo, 100, '=X', (gradient:45,0,#ab1212,1,#ac6060))");
expect(p.find('tw-meter')).toHaveBackgroundGradient(270, [
{stop:0,colour:"#AB1212"},
{stop:2,colour:"#AC6060"},
]);
p = runPassage("(set:$foo to 40)(meter: bind $foo, 100, 'X=', (stripes:125,14,#22FF00,#A00))");
expect(p.find('tw-meter')).toHaveBackgroundStripes(125, 14, ["#22FF00", "#AA0000"]);
});
/*
TODO: center meter tests
......
......@@ -392,6 +392,13 @@ describe("property indexing", function() {
expect("(print: (gradient:0,0.7,black,0.2,white)\'s stops\'s 1st's percent)").markupToPrint("0.2");
expect("(print: (gradient:0,0.2,black,0.7,white)\'s stops\'s last's percent)").markupToPrint("0.7");
expect("(print: (gradient:0,0.7,black,0.2,white)\'s stops\'s last's colour is black)").markupToPrint("true");
expect("(print: 'pixels' is in (gradient:0,0.2,black,0.7,white)\'s stops\'s last)").markupToPrint("false");
expect("(print: 'percent' is in (stripes:0,32,red,orange)\'s stops\'s last)").markupToPrint("false");
expect("(print: (stripes:0,32,red,orange)\'s stops\'s 1st's pixels)").markupToPrint("0");
expect("(print: (stripes:0,32,red,orange)\'s stops\'s 2nd's pixels)").markupToPrint("32");
expect("(print: (stripes:0,32,red,orange)\'s stops\'s last's pixels)").markupToPrint("64");
expect("(print: (stripes:0,32,red,orange)\'s stops\'s last's colour is orange)").markupToPrint("true");
});
it("no other values are present", function() {
expect("(print:(gradient:0,0,black,1,white)'s create)").markupToError();
......@@ -649,6 +656,13 @@ describe("property indexing", function() {
expect("(print: (gradient:0,0.7,black,0.2,white)'s ('stops')'s (1)'s 'percent')").markupToPrint("0.2");
expect("(print: (gradient:0,0.2,black,0.7,white)\'s ('stops')'s (-1)'s 'percent')").markupToPrint("0.7");
expect("(print: (gradient:0,0.7,black,0.2,white)\'s ('stops')'s (-1)'s 'colour' is black)").markupToPrint("true");
expect("(print: ('pixels') is in (gradient:0,0.2,black,0.7,white)\'s stops\'s (-1))").markupToPrint("false");
expect("(print: ('percent') is in (stripes:0,32,red,orange)\'s stops\'s last)").markupToPrint("false");
expect("(print: (stripes:0,32,red,orange)\'s ('stops')'s (1)'s pixels)").markupToPrint("0");
expect("(print: (stripes:0,32,red,orange)\'s stops\'s (2)'s pixels)").markupToPrint("32");
expect("(print: (stripes:0,32,red,orange)\'s ('stops')'s (-1)'s pixels)").markupToPrint("64");
expect("(print: (stripes:0,32,red,orange)\'s ('stops')'s (-1)'s colour is orange)").markupToPrint("true");
});
it("no other values are present", function() {
expect("(print:(gradient:0,0,black,1,white)'s 'create')").markupToError();
......
......@@ -279,7 +279,7 @@ var jasmineMatcher = (function() {
"background(?:-image)?:\\s*linear-gradient\\(" + degree + "deg,"
+ stops.reduce(function(str, stop) {
var colour = stop.colour.replace(/(\(|\))/g, "\\$1");
return str + "\\s*(?:" + colour + hexToRGBA(colour) + ")\\s+" + (stop.stop*100) + "%\\s*,"
return str + "\\s*(?:" + colour + hexToRGBA(colour) + ")\\s+" + (stop.stop*100) + "%,"
}, '').slice(0,-1)
+ "\\)", "i"));
......@@ -287,6 +287,39 @@ var jasmineMatcher = (function() {
return str + stop.colour + " " + (stop.stop*100) + "%, "
}, '').slice(0,-2) + ")";
return {
pass: !!pass,
message: !pass
? "Expected element style '" + a.attr('style')
+ "' to contain a background-image of " + message
: "Expected element style '" + a.attr('style')
+ "' not to contain a background-image of " + message
};
}
}),
toHaveBackgroundStripes: Object.bind(Object,{
compare: function(a,degree,dist,colours) {
var d = 0;
var pass = a.jquery && (a.attr('style') + '').match(new RegExp(
"background(?:-image)?:\\s*repeating-linear-gradient\\(" + degree + "deg,"
+ colours.reduce(function makeStops(str, colour) {
var ret = str + "\\s*(?:" + colour + hexToRGBA(colour) + ")\\s+" + d + "px,";
d += dist;
ret += "\\s*(?:" + colour + hexToRGBA(colour) + ")\\s+" + d + "px,";
return ret;
},'').slice(0,-1)
+ "\\)", "i"));
d = 0;
var message = "repeating-linear-gradient(" + degree + "deg, "
+ colours.reduce(function makeStops(str, colour) {
var ret = str + colour + " " + d + "px, ";
d += dist;
ret += str + colour + " " + d + "px, ";
return ret;
},'').slice(0,-2) + ")";
return {
pass: !!pass,
message: !pass
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment