wallside poast

This commit is contained in:
Jade Lovelace 2022-05-29 20:15:09 -07:00
parent 8e64a09f01
commit 61d04f2321
5 changed files with 519 additions and 0 deletions

View file

@ -0,0 +1,109 @@
+++
date = "2022-05-29"
draft = false
path = "/blog/WALL-side"
tags = ["art"]
title = "WALL side"
+++
{% image(name="wallside.png", colocated=true) %}
poster with diagonal WALL side text in four different
languages
{% end %}
My partner has a brilliant poster they made of the backing of 3M command strips
in their apartment, which I wanted to recreate as a vector image to make
another. I initially tried inkscape, where I ran into issues with the tiled
clone tool not supporting dragging to set spacing and more crucially not
supporting absolute distances, which meant that it could not maintain proper
spacing of things of different height (I later realized it could probably be
done with a group, but there were unrelated factors of fiddliness at play such
as difficulty working in a transformed coordinate system that made Inkscape
infeasible to use).
I conceded and did this project as a simple Python SVG generator. First, I took
a picture as reference, then included it in the SVG as an `<image>`. SVG is fun
because it is *not* HTML, and is also strict XML. For instance, one difference
is that the image tag is called `image` rather than `img` and uses an `href`,
not a `src` (or indeed, `xlink:href` if you are using an older implementation
such as inkscape).
With that out of the way, I made a `<g>` group that's rotated 45 degrees and
translated some amount (`transform="rotate(-45) translate(-1000, 100)"`), then
I created the four languages of `<text>` text elements inside. Regarding how to
get the actual text to put in there, there are various ways to do this; I typed
it in on my phone (including the Japanese! there's a drawing keyboard for
Japanese in Google Keyboard, so even my dubious-quality non-Japanese-speaker
scrawls got turned into characters pretty easily).
To get each text fragment into position easily,
I nicked [some code to make them draggable][draggable],
then noted down the transformation after dragging them into position. Next, I
duplicated each language's element and moved the new one into the next
horizontal position to figure out the horizontal (along the line) period and
the next vertical position to figure out the cross-line period.
Then, I made these definitions reusable by giving them an `id` property and
putting them in a `<defs>` block. This makes the original definition invisible,
so you have to reference them as something like `<use xlink:href="#someId" />`.
Since I knew the spacings, I got Python out in earnest. To make my workflow
more pleasant, I wanted to rebuild the image on every editor save. I found a
tool [`entr`][entr] that can do this: `ls *.py | entr -r python wallside.py`.
It takes a list of files to watch on standard input, and a command to run when
any of them change.
With my setup sufficiently pleasant, I wrote some Python to generate a series
of instances of the definition with the horizontal spacing for every language,
with each looking like this:
`<use x="{idx * SPACING[language]}" xlink:href="#{language}" />`.
This forms one line of several copies of each of English, French, Spanish, and
Japanese. I put this into a `<defs>` block as a group, then referenced it in
the body of the document with a `<use>` to check my work.
After I was satisfied this worked, I then started generating that `<use>`
automatically, with the `y` offset set to some multiple of the line spacing,
and with some tweaks, that was that.
Next was the job of getting it to work on Inkscape since I was prototyping
against Firefox, which, being a web browser, has a very advanced SVG renderer
compared to non-browser programs. One thing I was doing that was not ideal for
Inkscape was that I was rendering a bunch of text off-page. I fixed this with a
clip path the size of the document like so:
```xml
<clipPath id="viewRect">
<rect height="100%" width="100%" />
</clipPath>
<!-- ... -->
<g clip-path="url(#viewRect)">
<g transform="..."><!-- all the text goes in here --></g>
</g>
```
Another thing that Inkscape disliked (to the point of not rendering anything)
was the use of `href="..."` in my document. Its predecessor, `xlink:href`, was
[noted on MDN][mdn xlink] as being deprecated, replaced in the SVG 2 standard
by unprefixed `href`. I just had to switch to the older one and add
`xmlns:xlink="http://www.w3.org/1999/xlink"` to my `<svg>` element to fix this.
The last bit of trouble I got from Inkscape was that it does not support the
CSS `transform` property, so I had to convert to the `transform="..."` property
directly on tags. Oh well, so much for the shiny features. But it works now and
is more portable!
Finally, I have a SVG file that is exactly what I want and was not that painful
to create. That was fun!
I've included the sources and SVG file below (note: it requires Source Han Sans
installed on your computer, which is a nice open source sans-serif font with
Chinese/Japanese/Korean support).
* [SVG file here](./wallside.svg)
* [PDF for printing here (11x17 inch tabloid size)](./wallside.svg.pdf)
{{ codefile(path="wallside.py", colocated=true, code_lang="python", hide=true) }}
[draggable]: https://github.com/petercollingridge/code-for-blog/blob/master/svg-interaction/draggable/draggable_groups.svg
[mdn xlink]: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
[entr]: https://github.com/eradman/entr

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 KiB

View file

@ -0,0 +1,174 @@
import contextlib
class Periods:
y = 106
x = {
'en': 106,
'fr': 106,
'es': 146,
'jp': 82,
}
DEFS = """
"""
HEADER = """
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="17in" width="11in" onload="makeDraggable(evt)">
<style>
.text {
font-family: "Source Han Sans";
font-size: 14pt;
font-weight: bolder;
letter-spacing: -1.5px;
}
.text.japanese {
font-size: 16pt;
letter-spacing: -0.8px;
}
.wallside {
transform: rotate(-45deg)translate(-13in, 1.2in);
}
.draggable, .draggable-group {
cursor: move;
}
</style>
<!--<image href="./wallsideref.jpg" x="0" y="0" transform="scale(0.4, 0.4)" /> -->
<clipPath id="viewRect">
<rect height="17in" width="11in" />
</clipPath>
<rect height="17in" width="11in" stroke="black" fill="none" />
"""
FOOTER = """
<script type="text/javascript"><![CDATA[
function makeDraggable(evt) {
var svg = evt.target;
svg.addEventListener('mousedown', startDrag);
svg.addEventListener('mousemove', drag);
svg.addEventListener('mouseup', endDrag);
svg.addEventListener('mouseleave', endDrag);
svg.addEventListener('touchstart', startDrag);
svg.addEventListener('touchmove', drag);
svg.addEventListener('touchend', endDrag);
svg.addEventListener('touchleave', endDrag);
svg.addEventListener('touchcancel', endDrag);
function getMousePosition(evt) {
var CTM = svg.getScreenCTM();
if (evt.touches) { evt = evt.touches[0]; }
return {
x: (evt.clientX - CTM.e) / CTM.a,
y: (evt.clientY - CTM.f) / CTM.d
};
}
var selectedElement, offset, transform;
function initialiseDragging(evt) {
offset = getMousePosition(evt);
// Make sure the first transform on the element is a translate transform
var transforms = selectedElement.transform.baseVal;
if (transforms.length === 0 || transforms.getItem(0).type !== SVGTransform.SVG_TRANSFORM_TRANSLATE) {
// Create an transform that translates by (0, 0)
var translate = svg.createSVGTransform();
translate.setTranslate(0, 0);
selectedElement.transform.baseVal.insertItemBefore(translate, 0);
}
// Get initial translation
transform = transforms.getItem(0);
offset.x -= transform.matrix.e;
offset.y -= transform.matrix.f;
}
function startDrag(evt) {
if (evt.target.classList.contains('draggable')) {
selectedElement = evt.target;
initialiseDragging(evt);
} else if (evt.target.parentNode.classList.contains('draggable-group')) {
selectedElement = evt.target.parentNode;
initialiseDragging(evt);
}
}
function drag(evt) {
if (selectedElement) {
evt.preventDefault();
var coord = getMousePosition(evt);
transform.setTranslate(coord.x - offset.x, coord.y - offset.y);
}
}
function endDrag(evt) {
selectedElement = false;
}
}
]]></script>
</svg>
"""
BODY = """
<defs>
<text id="en" x="0" y="0" transform="translate(0, -80)" class="text">WALL side</text>
<text id="fr" x="0" y="0" transform="translate(-25, -56)" class="text">Côté MUR</text>
<text id="es" x="0" y="0" transform="translate(-28, 0)" class="text">lado de la PARED</text>
<text id="jp" x="0" y="0" transform="translate(-20, -27)" class="text japanese">かベ面</text>
</defs>
<g class="wallside">
<!--
<use href="#en" />
<use href="#en" x="106" class="draggable" />
<use href="#en" y="106" class="draggable" />
<use href="#fr" />
<use href="#fr" x="106" class="draggable" />
<use href="#es" />
<use href="#es" x="146" class="draggable" />
<use href="#jp" />
<use href="#jp" x="82" class="draggable" />
-->
</g>
"""
def make_line_def(id):
print(f'<defs><g id="{id}">')
for (lang, per) in Periods.x.items():
for i in range(30 if lang == 'jp' else 20):
print(f'<use x="{i * per}" xlink:href="#{lang}" />')
print('</g></defs>')
def make_wallside():
# 96px/in * {13in, 1.2in}
print('<g clip-path="url(#viewRect)"><g transform="rotate(-45) translate(-1248, 115)">')
for i in range(20):
print(f'<use xlink:href="#line-10" y="{Periods.y * i}" />')
print('</g></g>')
def main():
f = open('./wallside.svg', 'w')
with contextlib.redirect_stdout(f):
build()
print('built svg')
def build():
print(HEADER)
print(DEFS)
make_line_def('line-10')
make_wallside()
print(BODY)
print(FOOTER)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,236 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="17in" width="11in" onload="makeDraggable(evt)">
<style>
.text {
font-family: "Source Han Sans";
font-size: 14pt;
font-weight: bolder;
letter-spacing: -1.5px;
}
.text.japanese {
font-size: 16pt;
letter-spacing: -0.8px;
}
.wallside {
transform: rotate(-45deg)translate(-13in, 1.2in);
}
.draggable, .draggable-group {
cursor: move;
}
</style>
<!--<image href="./wallsideref.jpg" x="0" y="0" transform="scale(0.4, 0.4)" /> -->
<clipPath id="viewRect">
<rect height="100%" width="100%" />
</clipPath>
<rect height="17in" width="11in" stroke="black" fill="none" />
<defs><g id="line-10">
<use x="0" xlink:href="#en" />
<use x="106" xlink:href="#en" />
<use x="212" xlink:href="#en" />
<use x="318" xlink:href="#en" />
<use x="424" xlink:href="#en" />
<use x="530" xlink:href="#en" />
<use x="636" xlink:href="#en" />
<use x="742" xlink:href="#en" />
<use x="848" xlink:href="#en" />
<use x="954" xlink:href="#en" />
<use x="1060" xlink:href="#en" />
<use x="1166" xlink:href="#en" />
<use x="1272" xlink:href="#en" />
<use x="1378" xlink:href="#en" />
<use x="1484" xlink:href="#en" />
<use x="1590" xlink:href="#en" />
<use x="1696" xlink:href="#en" />
<use x="1802" xlink:href="#en" />
<use x="1908" xlink:href="#en" />
<use x="2014" xlink:href="#en" />
<use x="0" xlink:href="#fr" />
<use x="106" xlink:href="#fr" />
<use x="212" xlink:href="#fr" />
<use x="318" xlink:href="#fr" />
<use x="424" xlink:href="#fr" />
<use x="530" xlink:href="#fr" />
<use x="636" xlink:href="#fr" />
<use x="742" xlink:href="#fr" />
<use x="848" xlink:href="#fr" />
<use x="954" xlink:href="#fr" />
<use x="1060" xlink:href="#fr" />
<use x="1166" xlink:href="#fr" />
<use x="1272" xlink:href="#fr" />
<use x="1378" xlink:href="#fr" />
<use x="1484" xlink:href="#fr" />
<use x="1590" xlink:href="#fr" />
<use x="1696" xlink:href="#fr" />
<use x="1802" xlink:href="#fr" />
<use x="1908" xlink:href="#fr" />
<use x="2014" xlink:href="#fr" />
<use x="0" xlink:href="#es" />
<use x="146" xlink:href="#es" />
<use x="292" xlink:href="#es" />
<use x="438" xlink:href="#es" />
<use x="584" xlink:href="#es" />
<use x="730" xlink:href="#es" />
<use x="876" xlink:href="#es" />
<use x="1022" xlink:href="#es" />
<use x="1168" xlink:href="#es" />
<use x="1314" xlink:href="#es" />
<use x="1460" xlink:href="#es" />
<use x="1606" xlink:href="#es" />
<use x="1752" xlink:href="#es" />
<use x="1898" xlink:href="#es" />
<use x="2044" xlink:href="#es" />
<use x="2190" xlink:href="#es" />
<use x="2336" xlink:href="#es" />
<use x="2482" xlink:href="#es" />
<use x="2628" xlink:href="#es" />
<use x="2774" xlink:href="#es" />
<use x="0" xlink:href="#jp" />
<use x="82" xlink:href="#jp" />
<use x="164" xlink:href="#jp" />
<use x="246" xlink:href="#jp" />
<use x="328" xlink:href="#jp" />
<use x="410" xlink:href="#jp" />
<use x="492" xlink:href="#jp" />
<use x="574" xlink:href="#jp" />
<use x="656" xlink:href="#jp" />
<use x="738" xlink:href="#jp" />
<use x="820" xlink:href="#jp" />
<use x="902" xlink:href="#jp" />
<use x="984" xlink:href="#jp" />
<use x="1066" xlink:href="#jp" />
<use x="1148" xlink:href="#jp" />
<use x="1230" xlink:href="#jp" />
<use x="1312" xlink:href="#jp" />
<use x="1394" xlink:href="#jp" />
<use x="1476" xlink:href="#jp" />
<use x="1558" xlink:href="#jp" />
<use x="1640" xlink:href="#jp" />
<use x="1722" xlink:href="#jp" />
<use x="1804" xlink:href="#jp" />
<use x="1886" xlink:href="#jp" />
<use x="1968" xlink:href="#jp" />
<use x="2050" xlink:href="#jp" />
<use x="2132" xlink:href="#jp" />
<use x="2214" xlink:href="#jp" />
<use x="2296" xlink:href="#jp" />
<use x="2378" xlink:href="#jp" />
</g></defs>
<g clip-path="url(#viewRect)"><g transform="rotate(-45) translate(-1248, 115)">
<use xlink:href="#line-10" y="0" />
<use xlink:href="#line-10" y="106" />
<use xlink:href="#line-10" y="212" />
<use xlink:href="#line-10" y="318" />
<use xlink:href="#line-10" y="424" />
<use xlink:href="#line-10" y="530" />
<use xlink:href="#line-10" y="636" />
<use xlink:href="#line-10" y="742" />
<use xlink:href="#line-10" y="848" />
<use xlink:href="#line-10" y="954" />
<use xlink:href="#line-10" y="1060" />
<use xlink:href="#line-10" y="1166" />
<use xlink:href="#line-10" y="1272" />
<use xlink:href="#line-10" y="1378" />
<use xlink:href="#line-10" y="1484" />
<use xlink:href="#line-10" y="1590" />
<use xlink:href="#line-10" y="1696" />
<use xlink:href="#line-10" y="1802" />
<use xlink:href="#line-10" y="1908" />
<use xlink:href="#line-10" y="2014" />
</g></g>
<defs>
<text id="en" x="0" y="0" transform="translate(0, -80)" class="text">WALL side</text>
<text id="fr" x="0" y="0" transform="translate(-25, -56)" class="text">Côté MUR</text>
<text id="es" x="0" y="0" transform="translate(-28, 0)" class="text">lado de la PARED</text>
<text id="jp" x="0" y="0" transform="translate(-20, -27)" class="text japanese">かベ面</text>
</defs>
<g class="wallside">
<!--
<use href="#en" />
<use href="#en" x="106" class="draggable" />
<use href="#en" y="106" class="draggable" />
<use href="#fr" />
<use href="#fr" x="106" class="draggable" />
<use href="#es" />
<use href="#es" x="146" class="draggable" />
<use href="#jp" />
<use href="#jp" x="82" class="draggable" />
-->
</g>
<script type="text/javascript"><![CDATA[
function makeDraggable(evt) {
var svg = evt.target;
svg.addEventListener('mousedown', startDrag);
svg.addEventListener('mousemove', drag);
svg.addEventListener('mouseup', endDrag);
svg.addEventListener('mouseleave', endDrag);
svg.addEventListener('touchstart', startDrag);
svg.addEventListener('touchmove', drag);
svg.addEventListener('touchend', endDrag);
svg.addEventListener('touchleave', endDrag);
svg.addEventListener('touchcancel', endDrag);
function getMousePosition(evt) {
var CTM = svg.getScreenCTM();
if (evt.touches) { evt = evt.touches[0]; }
return {
x: (evt.clientX - CTM.e) / CTM.a,
y: (evt.clientY - CTM.f) / CTM.d
};
}
var selectedElement, offset, transform;
function initialiseDragging(evt) {
offset = getMousePosition(evt);
// Make sure the first transform on the element is a translate transform
var transforms = selectedElement.transform.baseVal;
if (transforms.length === 0 || transforms.getItem(0).type !== SVGTransform.SVG_TRANSFORM_TRANSLATE) {
// Create an transform that translates by (0, 0)
var translate = svg.createSVGTransform();
translate.setTranslate(0, 0);
selectedElement.transform.baseVal.insertItemBefore(translate, 0);
}
// Get initial translation
transform = transforms.getItem(0);
offset.x -= transform.matrix.e;
offset.y -= transform.matrix.f;
}
function startDrag(evt) {
if (evt.target.classList.contains('draggable')) {
selectedElement = evt.target;
initialiseDragging(evt);
} else if (evt.target.parentNode.classList.contains('draggable-group')) {
selectedElement = evt.target.parentNode;
initialiseDragging(evt);
}
}
function drag(evt) {
if (selectedElement) {
evt.preventDefault();
var coord = getMousePosition(evt);
transform.setTranslate(coord.x - offset.x, coord.y - offset.y);
}
}
function endDrag(evt) {
selectedElement = false;
}
}
]]></script>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.