Colors
A color in a wrun indicator lives in one of three places: as a literal on a
declaration, as a per-bar palette index on a color_by output, or as a
packed number that ./sdk/color computes for a drawing handle while the
module runs; in every place, seven theme tokens name the chart's own
colours instead of a fixed one.
This page covers the forms a declaration
accepts and where a name is refused, the theme tokens, the per-bar
ladder, and the run-time kit: ink, fromHex, alpha, mix, lighten,
darken, the channel readers, toPacked and fromPacked for the packed
colour an output carries, ColorScale and Thresholds.
Where a color lives
| Place | What it is | Who reads it |
|---|---|---|
a declaration (color, colors, borderColor, a renderer's color) | a string literal, or a param.color named as "@name" | Run reads it as text; the module never executes it |
a color_by output | a number per bar, floored into the colors palette | the chart, per bar |
a drawing handle (draw.line, draw.box, draw.label, draw.polyline) | a packed i32 from ./sdk/color or rgba(...) | the module, through color(...) and fill(...) while it runs |
The split is the one behind every declaration: the module computes numbers and the chart draws them from the declaration, so the decision behind a color (a regime, a bucket) stays a number your file can test, read at the Console prompt, or hand to a declared alert. What you would ask a color function for, and where each answer lives:
| You want | On a declaration | On a handle |
|---|---|---|
| an RGB color | a literal: "#2563eb", "rgb(37, 99, 235)", "hsl(221, 83%, 53%)" | ink.SKY, from, or rgba(14, 165, 233, 255) from ./gen/draw |
| transparency | the opacity option (0..1), or alpha inside the color ("#2563eb66") | alpha(c, 0.4) |
| a lighter, darker or blended color | write the result as a literal | lighten, darken, mix |
| a gradient driven by a value | a colors palette plus color_by over a bucket index: stepped, as fine as the palette is long | mix(a, b, t) per bar: continuous |
| a named palette | a colors literal array; there are no named palettes | ink.*, twenty named packed colors |
| a color the user picks | param.color, bound by "@name" (The Style page) | p_<name>() reads it as one packed number (Setting kinds) |
Color forms on declarations
The chart parses a declared color string as a CSS color. Every form, and where it is accepted:
| Form | Example | Accepted on |
|---|---|---|
| six-digit hex | "#2563eb" | everything |
| eight-digit hex, alpha in the last two digits | "#2563eb66" | everything; the built-in way to carry alpha |
rgb() / rgba() | "rgb(37, 99, 235)" | everything |
hsl() / hsla() | "hsl(221, 83%, 53%)" | everything |
| a named color | "orange" | an output's color and its colors palette, a segment, a box borderColor, render.*, draw.*, a range(); refused on a box fill |
"@name" | "@basis_color" | color and a colors entry, naming a param.color |
| a theme token | "theme.up" | everything: the chart's own colour, resolved when it paints (Theme tokens) |
A box's color is its fill, and the fill takes the box's opacity, so the
host must be able to rewrite the color with an alpha channel. That is why
a box color must be hex (3, 4, 6 or 8 digits), rgb() / rgba(),
hsl() / hsla() or a theme token: a name there is refused at validation with
color must be a hex, rgb() or hsl() color (the fill takes the opacity).
borderColor is a stroke and takes any form.
Alpha has two spellings. An eight-digit hex, rgba() or hsla() bakes it
into the color; the opacity option (0..1) on an output, a box, a range,
a fill, a handle or a card applies it on top. The two compose: a box with
color: "#2563eb" and opacity: 0.15 fills at fifteen percent, and an
output's opacity fades every colour it draws, a ladder's rungs and a
candle's border and wick included.
The sheet validator checks a box fill's form before the module runs; the
chart parses every other color string when it draws. A channel outside its
range (rgb(300, 0, 0)) is not a build error, but it is not a color the
chart can render either, so keep channels in 0..255 and hue, saturation
and lightness in their own ranges. Six- or eight-digit hex everywhere is
the safe habit: it validates on a fill, it carries alpha, and it is what
every worked example in this tree uses.
Theme tokens
Seven words name the chart's own colours instead of a fixed one, and
every colour key accepts them: theme.up and theme.down (the candle
colours), theme.text (the axis text), theme.muted (that text at 55
percent), theme.bg (the background), theme.grid (the grid lines) and
theme.accent (the app's accent). The chart resolves a token when it
paints and again when the theme switches, with no rerun, so an indicator
whose chrome is inked "theme.text" over "theme.bg" reads on a dark
and a light chart alike. A token carries no alpha of its own: fade it
through the opacity word of the surface (opacity, fill_opacity,
background_opacity) or, at run time, alpha(theme.UP, 0.2) from
./sdk/color, which also has the tokens as packed values for the
drawing handles (Theme colours).
A theme. word outside the seven is refused by name, and a
param.color default or preset stays a concrete colour: put the token
on the colour key, not on the setting.
The colour words that arrived with the styling round (an output's
fill_color, fill_gradient, gradient, up_color, down_color,
border_colors, wick_colors; a fill(...); a card, feed, meter, ladder
or HUD surface; a tile; a plot.levels profile and a panel.* series; a
frame's colour strings) take #rrggbb, #rrggbbaa or a theme token
only, one colour grammar on every new surface: the refusal reads
a colour must be #rrggbb, #rrggbbaa or one of theme.up, theme.down, theme.text, theme.muted, theme.bg, theme.grid, theme.accent. The older
surfaces (an output's color and colors, a segment, a box border, the
render.* and draw.* colours, a range()) keep taking a CSS name,
rgb() and hsl() beside the token, and the new keys of those surfaces
(background_color, border_color, glow_color, emblem_color,
text_color, fill_color, a gradient stop) take the same forms as
their siblings.
Named colors
The common names and the hex each one stands for. Write the hex instead of the name and the result is identical on every surface, including the one that refuses names.
| Name | Hex | Name | Hex | Name | Hex |
|---|---|---|---|---|---|
red | #FF0000 | green | #008000 | blue | #0000FF |
orange | #FFA500 | lime | #00FF00 | navy | #000080 |
yellow | #FFFF00 | olive | #808000 | teal | #008080 |
maroon | #800000 | purple | #800080 | aqua | #00FFFF |
fuchsia | #FF00FF | gray | #808080 | silver | #C0C0C0 |
black | #000000 | white | #FFFFFF |
A name and its hex are interchangeable on an output, and each output owns its color: there is no shared array with an index argument.
output("sma20", line, overlay, { color: "orange", width: 2 });
output("sma50", line, overlay, { color: "#FFA500", width: 2 }); // the same orangeChoosing colors
- Never rely on color alone: a
shapemark or a data-only output carries the same decision for anyone who cannot see the tint, and for an alert, which cannot see it at all. - Semantic colors read fastest: green for bullish signals and positive values, red for bearish signals and losses, blue for neutral lines and references, orange or yellow for warnings.
- Keep one scheme across related indicators. A package's colors are declarations, so they stay aligned across versions.
- Three sets that read well together:
green,red,bluefor signals;orange,purple,tealfor overlays;lime,yellow,fuchsiafor volume. - The worked examples in this tree use one muted set that reads on dark and
light themes alike:
#2563ebblue,#16a34agreen,#dc2626red,#f59e0bamber,#7c3aedviolet,#94a3b8slate for reference lines, and#22d3a5/#ff5b7ffor bull and bear tints. - Theme tokens for chrome, fixed colours for meaning. Ink a label's
background
theme.bg, its texttheme.textand a grid linetheme.grid, so the indicator follows the viewer's theme; keep the colours that carry a signal (green, red, amber) as hex, so they mean the same on every chart. - The light-theme guard. On a light chart the chart darkens a colour
that would vanish against the background; an indicator whose colours
are chosen for both themes (theme tokens, or a palette tested on both)
opts out with
chart.contrast_guard(false)at the top of the file and draws exactly as written.
Per-bar color
A declared color cannot change per bar, and the module cannot compute one
for a declaration. Per-bar color is a ladder: a data-only (none) output
holds a palette index, and the drawn output names it with color_by over
a literal colors array. Normalize the value into 0 .. n - 1, floor it,
write it to the none output, and put an n-entry palette on the drawn
output. What each bar draws:
| Index on this bar | The output draws |
|---|---|
| finite, inside the palette | that entry (floored) |
| finite, outside the palette | entry 0 |
NaN or infinite | its static color; entry 0 when none is declared |
color_by and colors go together: either without the other is refused
by name. An output cannot color itself, so the decision is its own output.
The ramp is as fine as the palette is long, and the palette is yours. Five
stops of a viridis ramp over RSI, a declared opacity, a two-literal blend
driven by a 0/1 index, and a box fill in hex:
param("period", 14, { min: 2, max: 200 });
// A five-step color ladder over RSI: cool when oversold, hot when overbought.
output("rsi", line, lower, { width: 2, color_by: "heat", colors: ["#440154", "#3b528b", "#21918c", "#5ec962", "#fde725"], description: "RSI colored by its own level" });
output("heat", none, lower, { description: "0..4: which fifth of 0..100 the RSI sits in" });
// opacity(color, 40) as a declared opacity.
output("sma20", line, overlay, { color: "#2563eb", opacity: 0.4, width: 2, description: "A faded average" });
// blend(green, red, weight) per regime as two literals and a 0/1 index.
output("trend", line, overlay, { width: 2, color_by: "trend_up", colors: ["#dc2626", "#16a34a"], description: "The average, red falling, green rising" });
output("trend_up", none, overlay, { description: "1 while the average rises" });
// A box fill: a hex color with the transparency in the declared opacity.
const bandHi = output("band_hi", none, overlay);
const bandLo = output("band_lo", none, overlay);
box("band", { top: bandHi, bottom: bandLo, color: "#2563eb", opacity: 0.12, borderWidth: 0 });
let rsi = new Rsi(14);
let sma = new Sma(20);
let average: f64 = NaN;
let prevAverage: f64 = NaN;
function onStart(): void {
rsi = new Rsi(i32(p_period()));
sma = new Sma(20);
}
function onBar(): void {
const close = bar.close();
const strength = rsi.update(close);
prevAverage = average;
average = sma.update(close);
if (isNaN(strength) || isNaN(average)) return;
// Bucket 0..100 into five steps; 100 itself lands in the last bucket.
let bucket = Math.floor(strength / 20.0);
if (bucket > 4.0) bucket = 4.0;
out_rsi(strength);
out_heat(bucket);
out_sma20(average);
out_trend(average);
out_trend_up(!isNaN(prevAverage) && average >= prevAverage ? 1.0 : 0.0);
out_band_hi(average * 1.01);
out_band_lo(average * 0.99);
}The heat output draws nothing, but it is a value like any other: after
a Run, type last 20 heat at the editor's Console prompt to read the
bucket on the newest twenty bars.
The index need not be a floor. A condition that answers 0, 1 or 2
is the same ladder over a three-entry palette, and a name is accepted as
an entry:
param("period", 14, { min: 2, max: 200, description: "RSI length" });
// colors is the palette; level picks the entry per bar: 0 red (overbought), 1 green (oversold), 2 blue (neutral).
output("rsi", line, lower, { width: 2, color_by: "level", colors: ["#FF0000", "green", "#0000FF"], description: "RSI with conditional colors" });
output("level", none, lower, { description: "0 above 70, 1 below 30, 2 in between" });
let rsi = new Rsi(14);
function onStart(): void {
rsi = new Rsi(i32(p_period()));
}
function onBar(): void {
const value = rsi.update(bar.close());
if (isNaN(value)) return;
out_rsi(value);
out_level(value > 70.0 ? 0.0 : value < 30.0 ? 1.0 : 2.0);
}The same pair works on a width_by output with a widths array for line
width, on a render.legend entry, and on a shape output
(Styling, Legend).
Tinting bars
The ladder also drives a background tint through render.bgcolor and a
candle tint through render.barcolor (Plotting):
a where gate says which bars get a tint, and color_by with colors
picks it per bar. A non-finite index means no tint on that bar (the static
color never substitutes), so a NaN bucket is a clean way to leave a
bar alone.
param("period", 14, { min: 2, max: 100, description: "RSI Period" });
param("overbought", 70, { min: 50, max: 100, description: "Overbought Level" });
param("oversold", 30, { min: 0, max: 50, description: "Oversold Level" });
output("rsi", line, lower, { color: "#2962ff", width: 2, description: "Relative strength index" });
output("zone", none, lower, { description: "0 oversold, 1 overbought: the tint ladder index; NaN in between" });
output("extreme", none, lower, { description: "1 on bars in either zone: the tint gate" });
// A background tint per condition: one declaration, a gate, and a two-entry ladder.
render.bgcolor("zones", { where: "extreme", color_by: "zone", colors: ["rgba(76, 175, 80, 0.3)", "rgba(244, 67, 54, 0.3)"] });
let rsi = new Rsi(14);
let overbought: f64 = 70.0;
let oversold: f64 = 30.0;
function onStart(): void {
rsi = new Rsi(i32(p_period()));
overbought = p_overbought();
oversold = p_oversold();
}
function onBar(): void {
const value = rsi.update(bar.close());
if (isNaN(value)) return;
const hot = value > overbought;
const cold = value < oversold;
out_rsi(value);
out_zone(cold ? 0.0 : hot ? 1.0 : NaN);
out_extreme(hot || cold ? 1.0 : 0.0);
}Five zones (extreme oversold, oversold, neutral, overbought, extreme
overbought) are a five-entry palette and a bucket that lands in
0 .. 4; the module above keeps two for clarity.
What has no form
A declared color is never computed at run time. Compute the color you
want ahead of time and write it as a literal; a blend that depends on a
per-bar weight is a ladder over a few blended literals, with the weight
bucketed into the index. The module has no color variable type: a declared
color is a string literal read once when the sheet is derived, and the
only setting that holds a color is a param.color. The one place a
computed color lands is a drawing handle, through the kit below.
Colors computed at run time
The line, box, label and polyline handles from ./gen/draw are created
and restyled by the module while it runs, and their color(...) and
fill(...) setters take a packed i32
(Drawing objects). That is where
./sdk/color goes: a color computed from a value on this bar, applied to
a handle on this bar. Its two scales, ColorScale and Thresholds, serve
the other half: they compute the index a color_by output carries, so the
color itself stays a literal and the decision stays a number.
Every export
| Export | Signature | Definition |
|---|---|---|
ink | ink.RED, ink.ORANGE, ink.AMBER, ink.YELLOW, ink.LIME, ink.GREEN, ink.EMERALD, ink.TEAL, ink.CYAN, ink.SKY, ink.BLUE, ink.INDIGO, ink.VIOLET, ink.PURPLE, ink.PINK, ink.ROSE, ink.SLATE, ink.GRAY, ink.WHITE, ink.BLACK (i32) | opaque packed colors, the palette below, packed exactly as rgba(...) from ./gen/draw packs |
fromHex | fromHex(hex: string): i32 | "#rrggbb" (opaque) or "#rrggbbaa", either case; any other text aborts with color: bad hex <x> |
alpha | alpha(color: i32, a: f64): i32 | the color with its alpha byte set from an opacity 0..1, RGB untouched |
mix | mix(a: i32, b: i32, t: f64): i32 | every channel, alpha included, t of the way from a to b, linear in sRGB |
lighten | lighten(color: i32, amount: f64): i32 | each RGB channel moved toward 255 by the fraction, alpha kept |
darken | darken(color: i32, amount: f64): i32 | each RGB channel moved toward 0 by the fraction, alpha kept |
red, green, blue | red(color: i32): i32 | one channel of a packed color, 0..255 (Pine's color.r, color.g, color.b) |
opacity | opacity(color: i32): f64 | the alpha byte over 255, 0..1: what alpha takes, so alpha(c, opacity(c)) is c again |
transparency | transparency( | Pine's color.t: (255 - alpha) * 100 / 255, 0 opaque to 100 invisible |
ColorScale | new ColorScale(min: f64, max: f64, steps: i32), .index(v: f64): f64 | floor((v - min) / (max - min) * steps) clamped to 0..steps - 1; NaN in, NaN out |
Thresholds | new Thresholds(, .index(v: f64): f64 | the number of levels at or below v, 0.; NaN in, NaN out |
theme | theme.UP, theme.DOWN, theme.TEXT, theme.MUTED, theme.BG, theme.GRID, theme.ACCENT (i32) | the seven theme tokens as packed values the chart resolves when it paints: the candle up and down colours, the axis text, that text at 55 percent, the background, the grid lines and the app's accent |
isThemeToken | isThemeToken(c: i32): bool | whether a packed value is one of the seven tokens (at any alpha) |
toPacked, fromPacked | toPacked(c: i32): f64, fromPacked(v: f64): i32 | a packed color as the f64 a color_packed_by output carries, and back; both keep a theme token a token |
Construct the scales and parse hex literals in onStart(): the
constructors allocate, and nothing else in the module allocates per bar.
Theme colours
A theme token is a colour the module names but never computes: the
chart fills it in when it paints, from its own theme, and again when the
theme switches, so a label inked theme.TEXT reads on a dark and a
light chart alike. alpha(theme.UP, 0.2) is still a token, carrying the
alpha with it, and opacity(c) and transparency(c) read that alpha
back; mix, lighten, darken, red, green and blue have no
concrete colour to work on and abort with color: <fn> takes a concrete colour; theme tokens resolve at paint time. A concrete colour never
spells a token by accident: an alpha-0 result whose red byte is 0x7e
and green byte 1..7 (an invisible colour before tokens existed) reads
as 0, transparent black, from rgba(...), alpha, mix, lighten,
darken, fromHex and fromPacked alike, so it stays invisible and
never aborts the readers. A token goes anywhere a
packed colour goes: a handle's color(...) or fill(...), and an output
a drawn output names with color_packed_by (write toPacked(theme.UP)
to it). The same seven words ride every declared colour as strings
("theme.up", Styling).
For a concrete colour the module can compute with, the chart hands over
its own: chart.up_color(), chart.down_color() and
chart.grid_color() declared at the top of the file (like
chart.bg_color()) are read in onStart() through
i32(p_chart_up_color()) and the two others as packed colours, 0 when
no chart is there to answer
(Chart context);
mix(i32(p_chart_up_color()), ink.WHITE, 0.3) is a lighter version of
the chart's own up colour. The chart's font needs no reader:
font_family: "ui" on any text resolves to the chart's own typography.
The palette
ink names the 500 row of the Tailwind palette plus white and black, each
opaque. The swatch column is the hex you would write as a declared literal
for the same color.
| Name | Swatch | Name | Swatch |
|---|---|---|---|
ink.RED | #ef4444 | ink.INDIGO | #6366f1 |
ink.ORANGE | #f97316 | ink.VIOLET | #8b5cf6 |
ink.AMBER | #f59e0b | ink.PURPLE | #a855f7 |
ink.YELLOW | #eab308 | ink.PINK | #ec4899 |
ink.LIME | #84cc16 | ink.ROSE | #f43f5e |
ink.GREEN | #22c55e | ink.SLATE | #64748b |
ink.EMERALD | #10b981 | ink.GRAY | #9ca3af |
ink.TEAL | #14b8a6 | ink.WHITE | #ffffff |
ink.CYAN | #06b6d4 | ink.BLACK | #000000 |
ink.SKY | #0ea5e9 | ink.BLUE | #3b82f6 |
A packed color is one i32:
((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff),
copied from rgba(...) in ./gen/draw bit for bit, so
ink.RED == rgba(239, 68, 68, 255) and the two mix freely. Because red
sits in the top byte, most inks are negative numbers when you print them:
that is the packing, not a fault.
An index from a value
ColorScale slices min..max into steps equal bands and answers the
band a value falls in, so a colors palette with steps entries on the
drawn output gets one entry per band. Thresholds answers how many of its
levels sit at or below the value, so a palette with one entry more than
the level count gets one entry per zone. Both return an f64 that goes
straight into a data-only output, and the drawn output names that output
with color_by. The RSI ladder with the kit computing the index, and a
zone number you can read after a Run with last 20 zone at the
Console prompt:
param("period", 14, { min: 2, max: 200, description: "RSI period" });
// Five bands of 0..100, coolest at the bottom: heat picks the entry per bar.
output("rsi", line, lower, { width: 2, color_by: "heat", colors: ["#3b82f6", "#0ea5e9", "#64748b", "#f97316", "#ef4444"], description: "RSI colored by its level" });
output("heat", none, lower, { description: "0..4: the fifth of 0..100 the RSI sits in" });
output("zone", none, lower, { description: "0 below 30, 1 between, 2 at or above 70" });
let rsi = new Rsi(14);
let scale = new ColorScale(0.0, 100.0, 5);
let zones = new Thresholds(new StaticArray<f64>(0));
function onStart(): void {
rsi = new Rsi(i32(p_period()));
// Built once here: index() never allocates, and neither holds bar state.
scale = new ColorScale(0.0, 100.0, 5);
const levels = new StaticArray<f64>(2);
levels[0] = 30.0;
levels[1] = 70.0;
zones = new Thresholds(levels);
}
function onBar(): void {
const strength = rsi.update(bar.close());
out_rsi(strength);
out_heat(scale.index(strength));
out_zone(zones.index(strength));
}An RSI of exactly 100 lands in the last band (the scale clamps, it never
answers steps), and a value on a threshold counts that threshold (70
is zone 2). A NaN RSI gives a NaN index, and the drawn output falls
back to its static color on that bar.
Colors on a handle
Handle setters take the packed color as it is computed, so here color can
follow a value continuously instead of through a ladder. The window mean
drawn as a line handle in ink.SKY, restyled to alpha(ink.SKY, 0.4) on
the newest bar (the forming one), and the window's range as a box whose
tint is mixed at run time between two hex literals parsed in onStart():
param("period", 20, { min: 2, max: 200, description: "Bars in the window" });
output("mean", line, overlay, { color: "#64748b", description: "The window mean" });
output("up_share", none, overlay, { description: "0..1: the share of up closes in the window, the box's mix weight" });
handles.line({ color: "#0ea5e9", width: 2 });
handles.box({ color: "#0ea5e9", opacity: 0.15, borderWidth: 1 });
const level = draw.line(0);
const span = draw.box(1);
let period: i32 = 20;
let sma = new Sma(20);
let ups = new Sma(20);
let highs = new Highest(20);
let lows = new Lowest(20);
let bull: i32 = 0;
let bear: i32 = 0;
let t: f64 = NaN;
let prevT: f64 = NaN;
let close: f64 = NaN;
let prevClose: f64 = NaN;
function onStart(): void {
period = i32(p_period());
sma = new Sma(period);
ups = new Sma(period);
highs = new Highest(period);
lows = new Lowest(period);
// Parse literals here: a bad string aborts with "color: bad hex <x>", and the message is readable from onStart().
bull = fromHex("#16a34a");
bear = fromHex("#dc2626");
}
function onBar(): void {
prevT = t;
t = bar.time();
prevClose = close;
close = bar.close();
const mean = sma.update(close);
const share = ups.update(!isNaN(prevClose) && close > prevClose ? 1.0 : 0.0);
const hi = highs.update(bar.high());
const lo = lows.update(bar.low());
if (isNaN(mean) || isNaN(hi) || isNaN(prevT)) return;
out_mean(mean);
out_up_share(share);
const start = t - (t - prevT) * f64(period - 1);
// The mean across the window: sky, and on the newest bar the same sky at 40% opacity.
level.set(start, mean, t, mean).color(bar.isLast() ? alpha(ink.SKY, 0.4) : ink.SKY);
// The window's range, tinted by the share of up closes: a gradient computed per bar, not a ladder.
const tone = mix(bear, bull, share);
span.set(start, hi, t, lo).fill(alpha(lighten(tone, 0.2), 0.15)).color(darken(tone, 0.3));
}bar.isLast() answers 1 on the newest bar the chart holds, so the line
is drawn faint while that bar is still forming and turns solid once the
next bar opens. The box's fill and border are two derivations of one
tone, so the pair always agree.
Read a color back
red, green and blue read one channel of a packed color, and
opacity and transparency read its alpha. The chart's background
arrives packed (chart.bg_color()), so a file can pick its ink from it:
dark words on a light chart, light words on a dark one.
// A price tag on the newest bar whose words follow the chart's background: dark on a light chart, light on a dark one.
chart.bg_color();
output("close_line", line, overlay, { color: "#94a3b8", description: "The close" });
output("brightness", none, overlay, { description: "The background's brightness, 0 (black) to 255 (white); NaN when the chart did not say" });
output("bg_opacity", none, overlay, { description: "How solid the background is, 0 to 1" });
string("tag", { max_bytes: 32 });
handles.label({ text: "tag", size: 12 });
const tag = draw.label(0);
let words: i32 = 0;
let brightness: f64 = NaN;
let bgOpacity: f64 = NaN;
function onStart(): void {
const bg = i32(p_chart_bg_color());
// 0 means the chart did not fill the color; then the words stay light.
if (bg != 0) {
brightness = 0.299 * f64(red(bg)) + 0.587 * f64(green(bg)) + 0.114 * f64(blue(bg));
bgOpacity = opacity(bg);
}
words = !isNaN(brightness) && brightness > 140.0 ? ink.BLACK : ink.WHITE;
}
function onBar(): void {
const close = bar.close();
if (isNaN(close)) return;
out_close_line(close);
out_brightness(brightness);
out_bg_opacity(bgOpacity);
if (!bar.isLast()) return;
sb_clear();
sb_text("last ");
sb_auto(close);
tag.set(bar.time(), close).text(str_tag_sb).color(words);
}- Brightness from the channels. The three channels weighted
0.299,0.587and0.114give 0 for black and 255 for white; above about 140 the background reads as light. On a cream background (#f4f1ea) the tag is black. - 0 means unknown. Where the chart did not fill the background,
p_chart_bg_color()reads 0 and the words stay white. - The alpha.
opacity(bg)is 1 for a solid background;transparency(bg)is the same reading on Pine's 0 to 100 scale, where 0 is solid. - Only a handle takes it. The computed color lands on the label
through
.color(...); a declared color stays a literal (What has no form, above).
The rules, written out
- Fractions. An opacity, a mix weight and a lighten or darken amount
are clamped to
0..1;NaNreads as0, soalpha(c, NaN)is invisible,mix(a, b, NaN)isa, andlighten(c, NaN)isc. - Rounding. A derived channel is
floor(x + 0.5)held to0..255:alpha(c, 0.5)sets the alpha byte to128,mix(ink.RED, ink.BLUE, 0.5)is#95639d,lighten(ink.BLUE, 0.5)is#9dc1fb,darken(ink.BLUE, 0.5)is#1e417b. - Readers.
red,greenandblueare the packed bytes, exact;alpha(c, opacity(c))rebuildscbit for bit;transparency(c)is(255 - alpha) * 100 / 255, so a color at alpha128reads49.80..., andalpha(c, 1.0 - transparency(c) / 100.0)iscagain. fromHex. Seven characters (#rrggbb, alpha255) or nine (#rrggbbaa), hex digits in either case. Anything else aborts the run withcolor: bad hex <x>and the string you passed. Call it inonStart(): an abort raised there shows its message in the Console, while an abort at module start (a top-levelconst c = fromHex(...)) reaches you as a bare failure with the text lost.ColorScale.vatmaxlands in the last band.stepsbelow1reads as1;maxbelowminruns the scale backwards;maxequal tominputs every value above it in the last band and the rest in band0. An infinite value clamps to the end it points at.Thresholds. A value exactly on a level counts it. The constructor copies the array, order does not matter, and aNaNlevel never counts.- State and allocation. Neither scale holds bar state, so a reset
has nothing to restore and a reset bar answers the same as any other.
index(),alpha,mix,lighten,darken, the five readers and a successfulfromHexnever allocate, so the per-bar path stays allocation-free under the module's runtime, which never frees.
From Pine
| Pine | wrun |
|---|---|
color.new(c, transp) | on a handle, alpha(c, (100 - transp) / 100): Pine counts transparency 0..100, alpha takes opacity 0..1; on a declaration, the opacity option |
color. | on a declaration, new Color into a color_by output over an n-entry colors palette from c1 to c2; on a handle, mix(c1, c2, t) |
color.rgb(r, g, b) | rgba(r, g, b, 255) from ./gen/draw (or rgb(r, g, b) there) |
color.r(c), color.g(c), color.b(c) | red(c), green(c), blue(c) on a packed handle color (a declared color is a literal the module never reads) |
color.t(c) | transparency(c), 0..100 as Pine counts it; opacity(c) is the same channel on the 0..1 scale alpha takes |