scratchblocks
Version:
Make pictures of Scratch blocks from text.
335 lines (289 loc) • 8.03 kB
JavaScript
/* for constructing SVGs */
// set by SVG.init
let document
let xml
const directProps = {
textContent: true,
}
export default class SVG {
static init(window) {
document = window.document
const DOMParser = window.DOMParser
xml = new DOMParser().parseFromString("<xml></xml>", "application/xml")
SVG.XMLSerializer = window.XMLSerializer
}
static makeCanvas() {
return document.createElement("canvas")
}
static cdata(content) {
return xml.createCDATASection(content)
}
static el(name, props) {
const el = document.createElementNS("http://www.w3.org/2000/svg", name)
return SVG.setProps(el, props)
}
static setProps(el, props) {
for (const key in props) {
const value = String(props[key])
if (directProps[key]) {
el[key] = value
} else if (
props[key] != null &&
Object.prototype.hasOwnProperty.call(props, key)
) {
el.setAttributeNS(null, key, value)
}
}
return el
}
static withChildren(el, children) {
for (const child of children) {
el.appendChild(child)
}
return el
}
static group(children) {
return SVG.withChildren(SVG.el("g"), children)
}
static newSVG(width, height, scale) {
return SVG.el("svg", {
version: "1.1",
width: width * scale,
height: height * scale,
viewBox: `0 0 ${width * scale} ${height * scale}`,
})
}
static polygon(props) {
return SVG.el("polygon", { ...props, points: props.points.join(" ") })
}
static path(props) {
return SVG.el("path", { ...props, path: null, d: props.path.join(" ") })
}
static text(x, y, content, props) {
const text = SVG.el("text", { ...props, x: x, y: y, textContent: content })
return text
}
static symbol(href) {
return SVG.el("use", {
href: href,
})
}
static move(dx, dy, el) {
SVG.setProps(el, {
transform: `translate(${dx} ${dy})`,
})
return el
}
/* shapes */
static rect(w, h, props) {
return SVG.el("rect", { ...props, x: 0, y: 0, width: w, height: h })
}
static roundRect(w, h, props) {
return SVG.rect(w, h, { ...props, rx: 4, ry: 4 })
}
static pillRect(w, h, props) {
const r = h / 2
return SVG.rect(w, h, { ...props, rx: r, ry: r })
}
static pointedPath(w, h) {
const r = h / 2
return [
`M ${r} 0`,
`L ${w - r} 0 ${w} ${r}`,
`L ${w} ${r} ${w - r} ${h}`,
`L ${r} ${h} 0 ${r}`,
`L 0 ${r} ${r} 0`,
"Z",
]
}
static pointedRect(w, h, props) {
return SVG.path({ ...props, path: SVG.pointedPath(w, h) })
}
static topNotch(w, y) {
return `c 2 0 3 1 4 2
l 4 4
c 1 1 2 2 4 2
h 12
c 2 0 3 -1 4 -2
l 4 -4
c 1 -1 2 -2 4 -2
L ${w - 4} ${y}
a 4 4 0 0 1 4 4`
}
static getTop(w) {
return `M 0 4
A 4 4 0 0 1 4 0
H 12 ${SVG.topNotch(w, 0)}`
}
static getRingTop(w) {
return `M 0 3
L 3 0
L 7 0
L 10 3
L 16 3
L 19 0
L ${w - 3} 0
L ${w} 3`
}
static getRightAndBottom(w, y, hasNotch, inset) {
if (typeof inset === "undefined") {
inset = 0
}
let arr = [`L ${w} ${y - 4}`, `a 4 4 0 0 1 -4 4`]
if (hasNotch) {
arr = arr.concat([
`L ${inset + 48} ${y}`,
"c -2 0 -3 1 -4 2",
"l -4 4",
"c -1 1 -2 2 -4 2",
"h -12",
"c -2 0 -3 -1 -4 -2",
"l -4 -4",
"c -1 -1 -2 -2 -4 -2",
])
}
if (inset === 0) {
arr.push("L", inset + 4, y)
arr.push("a 4 4 0 0 1 -4 -4")
} else {
arr.push("L", inset + 4, y)
arr.push("a 4 4 0 0 0 -4 4")
}
return arr.join(" ")
}
static getArm(w, armTop) {
return `L 16 ${armTop - 4}
a 4 4 0 0 0 4 4
L 28 ${armTop} ${SVG.topNotch(w, armTop)}`
}
static getArmNoNotch(w, armTop) {
return `L 16 ${armTop - 4}
a 4 4 0 0 0 4 4
L 28 ${armTop} L ${w - 4} ${armTop}
a 4 4 0 0 1 4 4`
}
static stackRect(w, h, props) {
return SVG.path({
...props,
path: [SVG.getTop(w), SVG.getRightAndBottom(w, h, true, 0), "Z"],
})
}
static capPath(w, h) {
return [SVG.getTop(w), SVG.getRightAndBottom(w, h, false, 0), "Z"]
}
static capRect(w, h, props) {
return SVG.path({ ...props, path: SVG.capPath(w, h) })
}
static getHatTop(w) {
return `M 0 16 c 25,-22 71,-22 96,0 L ${w - 4} 16 a 4 4 0 0 1 4 4`
}
static getCatTop(w) {
return `M 0 32
c2.6,-2.3 5.5,-4.3 8.5,-6.2c-1,-12.5 5.3,-23.3 8.4,-24.8c3.7,-1.8 16.5,13.1 18.4,15.4c8.4,-1.3 17,-1.3 25.4,0c1.9,-2.3 14.7,-17.2 18.4,-15.4c3.1,1.5 9.4,12.3 8.4,24.8c3,1.8 5.9,3.9 8.5,6.1
L ${w - 4} 32
a 4 4 0 0 1 4 4`
}
static hatRect(w, h, props) {
return SVG.path({
...props,
path: [SVG.getHatTop(w), SVG.getRightAndBottom(w, h, true, 0), "Z"],
})
}
static catHat(w, h, props) {
return SVG.group([
SVG.path({
...props,
path: [SVG.getCatTop(w), SVG.getRightAndBottom(w, h, true, 0), "Z"],
}),
SVG.move(
0,
32,
SVG.setProps(
SVG.group([
SVG.el("circle", {
cx: 29.1,
cy: -3.3,
r: 3.4,
}),
SVG.el("circle", {
cx: 59.2,
cy: -3.3,
r: 3.4,
}),
SVG.el("path", {
d: "M45.6,0.1c-0.9,0-1.7-0.3-2.3-0.9c-0.6,0.6-1.3,0.9-2.2,0.9c-0.9,0-1.8-0.3-2.3-0.9c-1-1.1-1.1-2.6-1.1-2.8c0-0.5,0.5-1,1-1l0,0c0.6,0,1,0.5,1,1c0,0.4,0.1,1.7,1.4,1.7c0.5,0,0.7-0.2,0.8-0.3c0.3-0.3,0.4-1,0.4-1.3c0-0.1,0-0.1,0-0.2c0-0.5,0.5-1,1-1l0,0c0.5,0,1,0.4,1,1c0,0,0,0.1,0,0.2c0,0.3,0.1,0.9,0.4,1.2C44.8-2.2,45-2,45.5-2s0.7-0.2,0.8-0.3c0.3-0.4,0.4-1.1,0.3-1.3c0-0.5,0.4-1,0.9-1.1c0.5,0,1,0.4,1.1,0.9c0,0.2,0.1,1.8-0.8,2.8C47.5-0.4,46.8,0.1,45.6,0.1z",
}),
]),
{
fill: "#000",
"fill-opacity": 0.6,
},
),
),
SVG.move(
0,
32,
SVG.el("path", {
d: "M73.1-15.6c1.7-4.2,4.5-9.1,5.8-8.5c1.6,0.8,5.4,7.9,5,15.4c0,0.6-0.7,0.7-1.1,0.5c-3-1.6-6.4-2.8-8.6-3.6C72.8-12.3,72.4-13.7,73.1-15.6z",
fill: "#FFD5E6",
transform: "translate(0, 32)",
}),
),
SVG.move(
0,
32,
SVG.el("path", {
d: "M22.4-15.6c-1.7-4.2-4.5-9.1-5.8-8.5c-1.6,0.8-5.4,7.9-5,15.4c0,0.6,0.7,0.7,1.1,0.5c3-1.6,6.4-2.8,8.6-3.6C22.8-12.3,23.2-13.7,22.4-15.6z",
fill: "#FFD5E6",
transform: "translate(0, 32)",
}),
),
])
}
static getProcHatTop(w) {
return `M 0 20 a 20 20 0 0 1 20 -20 L ${w - 20} 0 a 20,20 0 0,1 20,20`
}
static procHatRect(w, h, props) {
return SVG.path({
...props,
path: [SVG.getProcHatTop(w), SVG.getRightAndBottom(w, h, true, 0), "Z"],
})
}
static mouthRect(w, h, isFinal, lines, props) {
let y = lines[0].height
const p = [SVG.getTop(w), SVG.getRightAndBottom(w, y, true, 16)]
for (let i = 1; i < lines.length; i += 2) {
const isLast = i + 2 === lines.length
const line = lines[i]
y += line.height - 3
if (line.isFinal) {
p.push(SVG.getArmNoNotch(w, y))
} else {
p.push(SVG.getArm(w, y))
}
const hasNotch = !(isLast && isFinal)
const inset = isLast ? 0 : 16
y += lines[i + 1].height + 3
p.push(SVG.getRightAndBottom(w, y, hasNotch, inset))
}
p.push("Z")
return SVG.path({ ...props, path: p })
}
static commentRect(w, h, props) {
return SVG.roundRect(w, h, { ...props, class: "sb3-comment" })
}
static commentLine(width, props) {
return SVG.move(
-width,
9,
SVG.rect(width, 2, { ...props, class: "sb3-comment-line" }),
)
}
static strikethroughLine(w, props) {
return SVG.path({
...props,
path: ["M", 0, 0, "L", w, 0],
class: "sb3-diff sb3-diff-del",
})
}
}