Skip to content

Instantly share code, notes, and snippets.

@Herst
Last active December 30, 2020 13:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Herst/d5db2d3d1ea51a8ab8740e22ebaa16aa to your computer and use it in GitHub Desktop.
Save Herst/d5db2d3d1ea51a8ab8740e22ebaa16aa to your computer and use it in GitHub Desktop.
Outline for <text> and various experiments with it [II]
license: gpl-3.0
height: 600
scrolling: yes

If one wants to give a SVG <text> an outline effect, e.g. so it can be displayed on many different backgrounds (any color, dark, bright, patterns…), there are various approaches. In this demo three are shown (the first two are very similar though):

  • Example 1: Two <text> elements, the one with white thick stroke behind the other one.

  • Example 2: Similar to the previous example only here is only one <text> element and instead the paint-order attribute is used so the stroke is painted behing the fill. This attribute is not part of SVG 1.1 though and not supported by browsers such as Internet Explorer 11 or Microsoft Edge (as of EdgeHTML version 15).

  • Example 3: A <filter> with multiple filter effects merged inside in order to achieve the effect.

Panning/Zooming on the map is possible (e.g. using the mouse).

Additionally, there are various controls to test the behaviors especially in very dynamic applications.

A link can be created to start at a certain camera position or with certain default values. How to do it is left as an exercise to the reader.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Fun with &lt;text&gt;</title>
<style>
svg {
background-color: darkgreen;
width: 100%;
height: 300px;
overflow: hidden;
}
</style>
</head>
<body lang="en">
<div id="wrapper">
<svg>
<defs>
<filter id="whiteOutlineEffect" width="200%" height="200%" x="-50%" y="-50%" color-interpolation-filters="sRGB">
<feMorphology in="SourceAlpha" result="MORPH" operator="dilate" radius="1" />
<feColorMatrix in="MORPH" result="WHITENED" type="matrix" values="-1 0 0 0 1, 0 -1 0 0 1, 0 0 -1 0 1, 0 0 0 1 0" />
<feMerge>
<feMergeNode in="WHITENED" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
</svg>
</div>
<fieldset>
<label for="fontSize">Font Size [<code>&amp;fontSize=</code>]</label>
<input type="number" id="fontSize" step="1" min="1" max="300">
</fieldset>
<fieldset>
<label for="strokeWidth">Stroke Width (Examples 1 and 2) [<code>&amp;strokeWidth=</code>]</label>
<input type="number" id="strokeWidth" step="0.2" min="0" max="20">
</fieldset>
<fieldset>
<label for="radius">Radius (Example 3) [<code>&amp;radius=</code>]</label>
<input type="number" id="radius" step="1" min="0" max="20">
</fieldset>
<fieldset>
<label for="textRendering">Text Rendering [<code>&amp;textRendering=</code>]</label>
<select id="textRendering">
<option selected>auto</option>
<option>optimizeSpeed</option>
<option>optimizeLegibility</option>
<option>geometricPrecision</option>
</select>
</fieldset>
<fieldset>
<label for="rotation">Rotation [<code>&amp;rotation=</code>]</label>
<input type="number" id="rotation" step="3" min="0" max="360">
</fieldset>
<fieldset>
<label for="fontFamily">Font Family [<code>&amp;fontFamily=</code>]</label>
<input type="text" list="genericFamilies" id="fontFamily">
<datalist id="genericFamilies">
<option>serif</option>
<option>sans-serif</option>
<option>monospace</option>
<option>cursive</option>
<option>fantasy</option>
<option>system-ui</option>
</datalist>
</fieldset>
<fieldset>
<label for="strokeLineCap">Stroke Line Cap [<code>&amp;strokeLineCap=</code>]</label>
<select id="strokeLineCap">
<option value="" selected>&nbsp;</option>
<option>butt</option>
<option>round</option>
<option>square</option>
</select>
</fieldset>
<fieldset>
<label for="strokeLineJoin">Stroke Line Join [<code>&amp;strokeLineJoin=</code>]</label>
<select id="strokeLineJoin">
<option value="" selected>&nbsp;</option>
<option>miter</option>
<option>round</option>
<option>bevel</option>
</select>
</fieldset>
<p>[<code id="position"></code>]</p>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://cdn.rawgit.com/jerrybendy/url-search-params-polyfill/357fd6bf/index.js"></script>
<script>
/* global d3 */
"use strict";
var settings = {
fontSize: 35,
strokeWidth: 2,
radius: 1,
textRendering: "auto",
rotation: 0,
fontFamily: null,
strokeLineCap: null,
strokeLineJoin: null
};
var urlSearch = document.location.search || parent.document.location.search;
var searchParams = new URLSearchParams(urlSearch);
for (var key in settings) {
if (!settings.hasOwnProperty(key)) {
continue;
}
if (searchParams.has(key)) {
settings[key] = searchParams.get(key);
}
document.getElementById(key).value = settings[key] !== null ? settings[key] : "";
}
var wrapper = document.getElementById("wrapper");
var height = wrapper.offsetHeight;
var width = wrapper.offsetWidth;
var svg = d3.select("svg");
var outerG = svg.append("g");
var g = outerG.append("g");
var feMorphology = d3.select("#whiteOutlineEffect feMorphology");
var position = document.getElementById("position");
function zoomed() {
outerG.attr("transform", d3.event.transform);
var x = d3.event.transform.x;
var y = d3.event.transform.y;
var k = d3.event.transform.k;
x -= width / 2;
y -= height / 2;
position.innerHTML = [
"x=" + x.toFixed(3),
"y=" + y.toFixed(3),
"k=" + k.toFixed(3)
].join("&");
}
var zoom = d3.zoom()
.scaleExtent([1 / 8, 8])
.on("zoom", zoomed);
svg.call(zoom);
var x = 0, y = 0, k = 1;
if (searchParams.has("x") && searchParams.has("y") && searchParams.has("k")) {
x = +searchParams.get("x");
y = +searchParams.get("y");
k = +searchParams.get("k");
}
x += width / 2;
y += height / 2;
svg
.transition()
.duration(800)
.call(
zoom.transform,
d3.zoomIdentity
.translate(x, y)
.scale(k)
);
var example1 = g.append("g").attr("transform", "translate(0,-80)");
var ex1OutlineText = example1.append("text")
.attr("id", "ex1OutlineText")
.attr("stroke", "white")
.attr("fill", "none")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.text("Example 1");
var ex1Text = example1.append("text")
.attr("id", "ex1Text")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.text("Example 1");
var example2 = g.append("g").attr("transform", "translate(0,0)");
var ex2Text = example2.append("text")
.attr("id", "ex2Text")
.attr("stroke", "white")
.attr("paint-order", "stroke")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.text("Example 2");
var example3 = g.append("g").attr("transform", "translate(0,80)");
example3.attr("filter", "url(#whiteOutlineEffect)");
var ex3Text = example3.append("text")
.attr("id", "ex3Text")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.text("Example 3");
function update() {
feMorphology.attr("radius", settings.radius);
g.attr("transform", settings.rotation ? "rotate(" + settings.rotation + ")" : null);
ex1OutlineText
.attr("stroke-width", settings.strokeWidth)
.attr("stroke-linecap", settings.strokeLineCap)
.attr("stroke-linejoin", settings.strokeLineJoin)
.attr("font-size", settings.fontSize)
.attr("font-family", settings.fontFamily)
.attr("text-rendering", settings.textRendering);
ex1Text
.attr("stroke-linecap", settings.strokeLineCap)
.attr("stroke-linejoin", settings.strokeLineJoin)
.attr("font-size", settings.fontSize)
.attr("font-family", settings.fontFamily)
.attr("text-rendering", settings.textRendering);
ex2Text
.attr("stroke-width", settings.strokeWidth)
.attr("stroke-linecap", settings.strokeLineCap)
.attr("stroke-linejoin", settings.strokeLineJoin)
.attr("font-size", settings.fontSize)
.attr("font-family", settings.fontFamily)
.attr("text-rendering", settings.textRendering);
ex3Text
.attr("stroke-linecap", settings.strokeLineCap)
.attr("stroke-linejoin", settings.strokeLineJoin)
.attr("font-size", settings.fontSize)
.attr("font-family", settings.fontFamily)
.attr("text-rendering", settings.textRendering);
}
function windowResize() {
var zoomTransform = d3.zoomTransform(svg.node());
var k = zoomTransform.k,
x = zoomTransform.x,
y = zoomTransform.y;
x -= width / 2;
y -= height / 2;
height = wrapper.offsetHeight;
width = wrapper.offsetWidth;
zoom.extent([[0, 0], [width, height]]);
x += width / 2;
y += height / 2;
zoom.transform(svg, d3.zoomIdentity.translate(x, y).scale(k));
}
d3.select("#fontSize").on("change input textInput keyup blur", function () {
settings.fontSize = this.value || null;
update();
});
d3.select("#strokeWidth").on("change input textInput keyup blur", function () {
settings.strokeWidth = this.value || null;
update();
});
d3.select("#radius").on("change input textInput keyup blur", function () {
settings.radius = this.value || 0;
update();
});
d3.select("#textRendering").on("change input", function () {
settings.textRendering = this.value || null;
update();
});
d3.select("#rotation").on("change input textInput keyup blur", function () {
settings.rotation = this.value || 0;
update();
});
d3.select("#fontFamily").on("change input textInput keyup blur", function () {
settings.fontFamily = this.value || null;
update();
});
d3.select("#strokeLineCap").on("change input textInput keyup blur", function () {
settings.strokeLineCap = this.value || null;
update();
});
d3.select("#strokeLineJoin").on("change input textInput keyup blur", function () {
settings.strokeLineJoin = this.value || null;
update();
});
window.addEventListener("resize", windowResize);
update();
// Hello Chromium team!
function demo() {
console.log("start");
svg.transition().duration(2500)
.call(zoom.transform, d3.zoomIdentity.scale(1 / 8))
.on("end", function () {
svg.transition().duration(2500)
.call(zoom.transform, d3.zoomIdentity.scale(8))
.on("end", function () {
console.log("end");
});
});
}
function demoGood() {
document.body.style.fontFamily = "Roboto";
demo();
}
function demoBad() {
document.body.style.fontFamily = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\"";
demo();
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment