comicgen
Version:
Create comics for your website or app
133 lines (115 loc) • 6.25 kB
JavaScript
/* eslint-disable no-console */
import files from './files.json'
import { version } from '../package.json'
import { defaults, namemap, formats } from './characters.json'
export default function comicgen(selector, options) {
// Selector can be false-y, string selector or DOM node. Defaults to ".comicgen"
// Convert into a list of nodes
selector = selector instanceof Element ? [selector] : document.querySelectorAll(selector || '.comicgen')
// Render each node
Array.from(selector).forEach(node => {
// Identify the attributes to add from:
// 1. attrs = DOM node attributes (#1 priority)
// 2. options (#2 priority)
// 3. comicgen.defaults (#3 priority)
// This ensures that attrs follow the order of the node attribute keys.
for (var attrs = {}, j = 0, n = node.attributes; j < n.length; j++)
attrs[n[j].name] = n[j].value
attrs = Object.assign({}, comicgen.defaults, options, attrs)
// Based on the name= attribute, figure out which format to use from comicgen.formats.
// If it's an invalid format, report error and continue with the other elements.
var format = comicgen.formats[comicgen.namemap[attrs.name]]
if (!format)
return console.error('Unknown name="' + attrs.name + '" in', node)
// If any dirs variable is not set, report an error and continue
if (!format.dirs.every(attr => attr in attrs))
return console.error('Missing attr', format.dirs.join(', '), 'in', node)
// Create a mirror image transformation
var mirror = attrs.mirror ? `translate(${format.width},0)scale(-1,1)` : ''
// Build the SVG header
var svg = [
`<svg viewBox="${-attrs.x} ${-attrs.y} ${format.width} ${format.height}" width="${attrs.width || format.width}" height="${attrs.height || format.height}" preserveAspectRatio="xMidYMin slice">`,
`<g transform="scale(${attrs.scale})${mirror}">`
]
let continuousUrls = []
// Loop through all attributes (e.g. emotion=, pose=, body=, etc)
// If the attribute is in format.file, there's an image for it. Add it.
for (var attr in attrs) {
if (attr in format.files) {
var row = format.files[attr]
// Substitute any $variable with the corresponding attribute value
if (row.continuous) {
files[attrs['name']][attr].forEach(filename => {
// replace row.continuous (ex: face, body) with filename (ex: meh, surprise)
let img = row.file.replace(/\$([a-z]*)/g, (match, group) => group === row.continuous ? filename : attrs[group])
continuousUrls.push({
fetch: fetch(`${comicgen.base}svg/${img}.svg`).then(res => res.text()),
sliderVal: attrs[attr]
})
})
}
else {
var img = row.file.replace(/\$([a-z]*)/g, (match, group) => attrs[group])
svg.push(`<image width="${row.width}" height="${row.height}" transform="translate(${row.x},${row.y})" xlink:href="${comicgen.base}${attrs.ext}/${img}.${attrs.ext}"/>`)
}
}
}
continuousUrls.length && Promise.all(continuousUrls.map(d => d.fetch))
.then(svg_responses => {
let character_svg_container = node.querySelector('svg g')
character_svg_container.innerHTML = ''
// One body part is interpolated with 2 consecutive svg responses.
for (let i = 0; i < continuousUrls.length; i = i + 2) {
character_svg_container.innerHTML += `<g>${svg_responses[i]}<template>${svg_responses[i]}</template>
<template>${svg_responses[i + 1]}</template></g>`
// pass the just inserted svg node to function create_parametric_svg
create_parametric_svg(character_svg_container.querySelector(`svg g:nth-of-type(${i/2+1})`), continuousUrls[i].sliderVal)
}
})
// Add the SVG footer
svg.push('</g></svg>')
node.innerHTML = svg.join('')
// TODO: trigger an event
})
}
function create_parametric_svg(node, sliderVal) {
let character_svg_nodes = node.querySelectorAll(':scope > svg g *')
Array.from(character_svg_nodes).forEach(character_svg_node => {
// TODO: Refactor to remove use of IDs
let startnode = node.querySelector(`template:nth-of-type(1) #${character_svg_node.id}`)
let endnode = node.querySelector(`template:nth-of-type(2) #${character_svg_node.id}`)
Array.from(character_svg_node.attributes)
.map(d => d.nodeName)
.forEach(attr =>
character_svg_node.setAttribute(attr,
attr === 'd' ?
// For smoother paths and worse performance, reduce "maxSegmentLength" value (defaults to 10).
flubber.interpolate(startnode.getAttribute(attr), endnode.getAttribute(attr), { maxSegmentLength: 5 })(sliderVal)
:
d3.interpolate(startnode.getAttribute(attr), endnode.getAttribute(attr))(sliderVal)
)
)
})
}
// If this script is loaded as: -> Then this is the base location
// https://cdn.jsdelivr.net/npm/comicgen -> https://cdn.jsdelivr.net/npm/comicgen/
// http://unpkg.com/comicgen -> http://unpkg.com/comicgen/
// https://gramener.com/comicgen/dist/comicgen.min.js -> https://gramener.com/comicgen/
// node_modules/comicgen/dist/comicgen.min.js -> node_modules/comicgen/
let base = document.currentScript ? document.currentScript.src : 'https://gramener.com/comicgen'
// If there's a .js at the end, remove dist/comicgen.min.js to get the base location
comicgen.base = (base + '/').replace(/[a-z]*\/[a-z.]*\.js\/$/, '')
// Import comicgen version from package.json
comicgen.version = version
// Import character configurations from characters.json
comicgen.defaults = defaults
comicgen.namemap = namemap
comicgen.formats = formats
// This script could be loaded async or not. If async, DOMContentLoaded is already executed.
// So if readyState is not 'loading' (i.e. 'interactive' or 'complete'), run comicgen directly.
// https://javascript.info/onload-ondomcontentloaded
// https://github.com/jquery/jquery/blob/master/src/core/ready.js
if (document.readyState == 'loading')
document.addEventListener('DOMContentLoaded', () => comicgen())
else
window.setTimeout(comicgen)