Public
Edited
Aug 9, 2024
Importers
8 stars
Insert cell
Insert cell
examples = [
HatchTile0({fill: preview_fill, fg: "#47f"}),
HatchTile45({fill: preview_fill, fg: "#47f"}),
HatchTile90({fill: preview_fill, fg: "#47f"}),
HatchTile135({fill: preview_fill, fg: "#47f"}),
HalfToneTile({fill: preview_fill, fg: "#47f", fill_alldots: .32}),
HalfToneTile8({fill: preview_fill, fg: "#47f", fill_alldots: .32}),
CrossHatchTile45({fill: preview_fill, fg: "#47f"}),
CrossHatchTile90({fill: preview_fill, fg: "#47f"}),
DahsedHatchTile({fill: preview_fill, fg: "#47f"})]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
h1 = new HatchTile45({fill: .4, fg: "#3e7", id: "my_id"});
Insert cell
show([h1,
h1.with({fg:"#37f", bg:"#fffd66"}),
h1.with({size:4, fg: "#777"}),
h1.with({size:8 , fill:.8}),
HatchTile135(h1)])
Insert cell
Insert cell
{
var s = {fg: "#76d"};
return show([
HatchTile45(s),
CrossHatchTile45(s),
CrossHatchTile90(s),
// the generic CrossHatchTile() function takes one of the simple hatch tile generators
CrossHatchTile(s, HatchTile45, HatchTile0)]);
}
Insert cell
Insert cell
{
var h = DahsedHatchTile({fg: "#d46"});
return show([
h,
h.with({size: [20, 5], fill: .7, dashes: .75, cap:"round", dashOffsets: [0]}),
h.with({rotate: 0, size: [20, 10], dashes: [.15, .05, .75, .05], dashOffsets: [0, .3]})])
}
Insert cell
Insert cell
show([0, .03, .1, .2, .3, .5, .7, .8, .9, .97, 1].map(
x => HalfToneTile({fill: x, fg: "#66a"})
), 50)
Insert cell
Insert cell
{
var l = [0, .01, .025, .05, .1, .15, .2, .3, .4, .6, .9]
return html`
${show(l.map(x => HalfToneTile({fill_alldots: 0, fill: x, fg: "#66a"})), 50)}
${show(l.map(x => HalfToneTile({fill_alldots: .4, fill: x, fg: "#66a"})), 50)}
${show(l.map(x => HalfToneTile8({fill_alldots: .4, fill: x, fg: "#66a"})), 50)}`
}
Insert cell
Insert cell
show([StackedTile([
HatchTile45({bg: "#fffd88", fg: "#c59", fill: .3, size: 15}),
HatchTile135({bg: "#blue", fg: "#37d", fill: .3, size: 2})])
]);
Insert cell
Insert cell
Insert cell
Insert cell
HatchTile0 = _define_pattern(
"hatch-0",
{ ..._defaultHatch, size: 5},
([s, h], f, set) => `<rect y="${(.5*(s-f))}" width="${s}" height="${f}" fill="${set.fg}"/>`
)
Insert cell
Insert cell
HatchTile45 = _define_pattern(
"hatch-45",
{ ..._defaultHatch, size: 8},
([s, h], f, set) => `<path
d="M0 ${f*.5} L${f*.5} 0 L0 0 Z M${s-f*.5} ${s} ${s} ${s-f*.5} L${s} ${s} Z
M0 ${s} L${.5*f} ${s} L${s} ${.5*f} L${s} 0 L${s-.5*f} 0 L0 ${s-.5*f} Z"
fill="${set.fg}" stroke="none" />`
)
Insert cell
Insert cell
HatchTile90 = _define_pattern(
"hatch-90",
{ ..._defaultHatch, size: 5},
([s, h], f, set) => `<rect x="${(.5*(s-f))}" width="${f}" height="${s}" fill="${set.fg}"/>`
)
Insert cell
Insert cell
HatchTile135 = _define_pattern(
"hatch-135",
{ ..._defaultHatch, size: 8},
([s, h], f, set) => `<path
d="M${s} ${.5*f} L${s} 0 L${s - .5*f} 0 Z M0 ${s-.5*f} L0 ${s} L${.5*f} ${s} Z
M${s} ${s} L${s} ${s-.5*f} L${.5*f} 0 L0 0 L0 ${.5*f} L${s-.5*f} ${s} Z"
fill="${set.fg}" stroke="none" />`
)
Insert cell
Insert cell
HalfToneTile = _define_pattern(
"halftone",
{ ..._defaultHatch, size: 8, offset: [.5, .5], fill_alldots: 0},
function (s, f, set)
{
s = s[0];
var s_fill = set.fill;
if (s_fill <= 0) return "";
// fill. For low fills, fade the corners out first
var fill1 = s_fill > set.fill_alldots ?
s_fill : Math.min(set.fill_alldots, 2 * set.fill);
var fill2 = 2 * s_fill - fill1;
// not 100% accurate between 80% and 100% but it is close
var r1 = fill1 < 0.785398
? Math.sqrt(fill1 * .5 / Math.PI)
: .5 - Math.sqrt((1 - fill1) * 0.31396 / Math.PI);
var r2 = fill1 > set.fill_alldots ? r1 : Math.sqrt(fill2 * .5 / Math.PI);
r1 *= s;
r2 *= s;
// pattern
var dot = (x, y, r) => `<circle cx="${x}" cy="${y}" r="${r}" fill="${set.fg}" stroke="none" />`;
var ss = "";
ss += dot(s*.5, s*.5, r1);
for (var [x, y] of [[0,0], [0,s], [s,0], [s,s]]) {
ss += dot(x, y, r2)
}
return ss;
}
)
Insert cell
Insert cell
HalfToneTile8 = _define_pattern(
"halftone8",
{ ..._defaultHatch, size: 16, offset: [.5, .5], fill_alldots: 0},
function (s, f, set)
{
s = s[0];
var s_fill = set.fill;
if (s_fill <= 0) return "";
// tile layout, 4 levels. Lower case indicates copies due to
// the pattern wrapping around:
// B···C···b
// : D D :
// C A c
// : D D :
// b···c···b
const levels = [
[[0.5*s, 0.5*s]], // A: middle dot
[[0,0], [0,s], [s,0], [s,s]], // B: corner dot (4 instances)
[[0.5*s,0], [0,0.5*s], [0.5*s, s], [s,0.5*s]], // C: 2 side dots (2 inst. each)
[[0.25*s,0.25*s], [0.25*s,0.75*s], [0.75*s, 0.25*s], [0.75*s,0.75*s]], // D: 4 middle dots of quadrants
];
const level_n = [1, 1, 2, 4];
const n = 8;
var r = [];
if (s_fill < set.fill_alldots)
{
var remaining = s_fill;
for (var i = 0; i < 4; ++i)
{
var this_fill = Math.min(remaining / level_n[i], set.fill_alldots / n);
remaining -= this_fill * level_n[i];
var this_r = Math.sqrt(n * this_fill * .125 / Math.PI);
r.push(this_r * s);
}
}
else
{
var r1 = s_fill < 0.785398
? Math.sqrt(s_fill * .125 / Math.PI)
: .25 - Math.sqrt((1 - s_fill) * 0.07849 / Math.PI);
r1 *= s;
r = [r1, r1, r1, r1];
}

// pattern
var dot = (x, y, r) => `<circle cx="${x}" cy="${y}" r="${r}" fill="${set.fg}" stroke="none" />`;
var ss = "";
for (var i = 0; i < 4; ++i)
{
for (var [x, y] of levels[i]) {
ss += dot(x, y, r[i])
}
}
return ss;
}
)
Insert cell
Insert cell
CrossHatchTile45 = settings => CrossHatchTile(settings, HatchTile45, HatchTile135)
Insert cell
Insert cell
CrossHatchTile90 = settings => CrossHatchTile(settings, HatchTile0, HatchTile90)
Insert cell
Insert cell
CrossHatchTile =
{
var _CrossHatchTile = _define_pattern(
"cross",
{..._defaultHatch, size: 8, _F1: undefined, _F2: undefined},
function(s, f, set) {
var ss = "";
for (var p of set._parts)
{
ss += p._svg_fg(s, p.settings.fill * s[0], p.settings);
}
return ss;
},
function(set) {
// compensate fill: the actual fill factor will be s.fill * (2 - s.fill)
if (!set._F1) return;
var hatch1 = set._F1({...set, id:""});
var s = hatch1.settings;
s.fill = 1 - Math.sqrt(1 - s.fill);
set._parts = [hatch1, set._F2({...s, id:""})];
}
);
// compensate fill: the actual fill factor will be s.fill * (2 - s.fill)
return function(settings, F1, F2) {
var p1 = F1({settings, id:""});
console.log(p1.settings);
var id = settings && settings.id
return _CrossHatchTile({...p1.settings, id, _F1:F1, _F2:F2});
}
}
Insert cell
Insert cell
DahsedHatchTile = _define_pattern(
"hatch-dashed",
{ ..._defaultHatch,
size: [10, 10], dashes: .75, dashOffsets:[0, .5], rotate: -45, cap: "but"},
function([w, h], f, set) {
h = h / set.dashOffsets.length;
var y = h * .5;
var ss = "";
set._dashoffsets.forEach((o, i)=>
{
ss += `<line
x1="-w" x2="${2*w}"
y1="${y}" y2="${y}"
stroke="${set.fg}" stroke-linecap="${set.cap}"
stroke-width="${set.fill * h}" stroke-dasharray="${set._dasharray}" stroke-dashoffset="${o}"
fill="${set.fg}"/>`
y += h;
});
return ss;
},
function(set) {
if (!Array.isArray(set.size)) { set.size = [set.size, set.size]; }
if (!Array.isArray(set.dashes))
{
set.dashes = [ set.dashes, 1 - set.dashes];
}
set._dasharray = set.dashes.map(x => set.size[0] * x).join(",");
set._dashoffsets = set.dashOffsets.map(x => set.size[0] * x);
}
);
Insert cell
Insert cell
StackedTile = {
var _Stack = _define_pattern(
"stacked",
{..._defaultSettings, parts: []},
function (s, f, set)
{
var ss = "";
for (var p of set.parts)
{
ss += p._svg_fg(s, p.settings.fill * s[0], p.settings);
}
return ss;
}
)
function make(parts, id)
{
// inherit size and background from first part
var stack = _Stack(parts[0]);
stack.settings.parts = parts;
if (id) { stack.id = id; }
return stack;
}
return make;
}
Insert cell
Insert cell
BlankTile = _define_pattern(
"blank",
_defaultSettings,
(s, f, settings) => "")

Insert cell
Insert cell
_defaultSettings = ({
rotate: 0,
scale: 1,
offset: [0, 0],
size : 1,
bg : "white",
})
Insert cell
_defaultHatch = ({
..._defaultSettings,
fill : .5,
fg : "black",
})
Insert cell
function show(patterns, size, round)
{
// wishes:
// - the border occupies exactly 1 pixel (it is a myth that
// you can ignore physical pixels for SVG);
// - size specifies the size of the area inside the border;
// - in pattern space (userSpaceOnUse), (0, 0) is exactly
// at the corner inside the non-rounded border.
// concessions:
// - we do not attempt to compensate for device pixel ratio. So the result will look bad
// at 1.25 and 1.50 ratios.
size = size || 100;
var size2 = size + 2;
var sizeR = round != false ? size * .1 : 0;
var s = "";
for (const p of patterns)
{
s += `<svg viewBox="0 0 ${size2} ${size2}" width="${size2}" height="${size2}" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" style='margin-right: ${size * .1}px;'>`;
s += p.svg();
s += `<rect transform="translate(1, 1)" x="-0.5" y="-0.5" width="${size+1}" height="${size+1}"
rx="${sizeR}" ry="${sizeR}"
fill="${p.url()}" stroke-width="1" stroke="#777" />`;
s += `</svg>`;
}
return html`<div>${s}</div>`;
}
Insert cell
function _define_pattern(typeId, defaults, paint_fg, extra_init)
{
function size2(x) { return Array.isArray(x) ? x : [x, x]; }
function _PatternBase(settings) {
this.id = "";
this.settings = settings || this.defaults;
this.size = size2(this.settings.size);
this.extra_init(this.settings);
}
_PatternBase.prototype.typeId = typeId;
_PatternBase.prototype.defaults = defaults;
_PatternBase.prototype._svg_fg = paint_fg;
_PatternBase.prototype.extra_init = extra_init || function(){};

_PatternBase.prototype._new_settings = function(new_settings, id)
{
var s = {...this.settings, ...new_settings};
this.settings = s;
if (id == undefined) {
this.uid = DOM.uid("pattern-" + this.typeId);
this.id = this.uid.id;
}
else if (id.id) {
this.uid = id;
this.id = id.id;
}
else {
this.id = id.toString();
}
this.size = size2(s.size);
this.extra_init(this.settings);
}
_PatternBase.prototype.svg_fg = function()
{
var s = this.settings.size;
var f = this.settings.fill * s;
return this._svg_fg(s, f, this.settings)
};

_PatternBase.prototype.svg = function()
{
var s = this.size;
var f = this.settings.fill * s[0];
var offset = this.settings.offset;
return `
<pattern id="${this.id}"
x="${offset[0]}" y="${offset[1]}"
patternUnits="userSpaceOnUse" width="${s[0]}" height="${s[1]}"
patternTransform="rotate(${this.settings.rotate}) scale(${this.settings.scale})">
<rect width="${s[0]}" height="${s[1]}" fill="${this.settings.bg}"/>
${this._svg_fg(s, f, this.settings)}
</pattern>`;
};

_PatternBase.prototype.url = function()
{
return this.uid ? this.uid.toString() : `url(#${this.id})`
};

// copy with optionally new settings
_PatternBase.prototype.with = function(new_settings)
{
var p = new _PatternBase(this.settings);
p._new_settings(new_settings || {}, new_settings && new_settings.id);
return p;
};

function make(new_settings)
{
// if we have a settings field, assume it is another pattern
// otherwise assume it is a settings object
new_settings = new_settings || {}
if (new_settings.settings)
{
new_settings = new_settings.settings;
}

var p = new _PatternBase();
p._new_settings(new_settings, new_settings.id);
return p;
}
return make;
}
Insert cell
md`## Imports`
Insert cell
import { Range } from "@observablehq/inputs"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more