forked from transitive-bullshit/primitive
-
Notifications
You must be signed in to change notification settings - Fork 0
/
module.js
126 lines (108 loc) · 3.5 KB
/
module.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// node es module entrypoint
import ow from 'ow'
import path from 'path'
import rmfr from 'rmfr'
import tempy from 'tempy'
import Time from 'time-diff'
import context from './lib/context'
import primitive from './lib/primitive'
const supportedOutputFormats = new Set([
'png',
'jpg',
'svg',
'gif'
])
/**
* Reproduces the given input image using geometric primitives.
*
* Returns a Promise for the generated model.
*
* Available output formats:
* - png
* - jpg
* - svg
* - gif
*
* @name primitive
* @function
*
* @param {Object} opts - Configuration options
* @param {string} opts.input - Input image to process (can be a local path, http url, or data url)
* @param {string} [opts.output] - Path to generate output image
* @param {number} [opts.numSteps=200] - Number of steps to process [1, 1000]
* @param {number} [opts.minEnergy] - Minimum energy to stop processing early [0, 1]
* @param {number} [opts.shapeAlpha=128] - Alpha opacity of shapes [0, 255]
* @param {string} [opts.shapeType=traingle] - Type of shapes to use
* @param {number} [opts.numCandidates=1] - Number of top-level candidates per step [1, 32]
* @param {number} [opts.numCandidateShapes=50] - Number of random candidate shapes per step [10, 1000]
* @param {number} [opts.numCandidateMutations=100] - Number of candidate mutations per step [10, 500]
* @param {number} [opts.numCandidateExtras=0] - Number of extra candidate shapes per step [0, 16]
* @param {function} [opts.onStep] - Optional async function taking in the model and step index
* @param {function} [opts.log=noop] - Optional logging function (console.log to enable logging)
*
* @return {Promise}
*/
export default async (opts) => {
const {
input,
output,
onStep,
numSteps = 200,
nthFrame = 0,
...rest
} = opts
ow(opts, ow.object.label('opts'))
ow(input, ow.string.nonEmpty.label('input'))
ow(nthFrame, ow.number.integer)
ow(numSteps, ow.number.integer.positive.label('numSteps'))
if (output) ow(output, ow.string.nonEmpty.label('output'))
const ext = output && path.extname(output).slice(1).toLowerCase()
const isGIF = (ext === 'gif')
if (output && !supportedOutputFormats.has(ext)) {
throw new Error(`unsupported output format "${ext}"`)
}
const target = await context.loadImage(input)
const tempDir = isGIF && tempy.directory()
const tempOutput = isGIF && path.join(tempDir, 'frame-%d.png')
const frames = []
const { model, step } = await primitive({
...rest,
context,
target,
onStep: async (model, step) => {
if (onStep) await onStep(model, step)
if (isGIF) {
if (nthFrame <= 0 || (step - 1) % nthFrame === 0) {
const frame = tempOutput.replace('%d', frames.length)
await context.saveImage(model.current, frame)
frames.push(frame)
}
} else if (output) {
if (nthFrame > 0 && (step - 1) % nthFrame === 0) {
const frame = output.replace('.', `-${step - 1}.`)
await context.saveImage(model.current, frame, opts)
}
}
}
})
const time = new Time()
for (let s = 1; s <= numSteps; ++s) {
time.start(`step ${s}`)
const candidates = await step(s)
console.log(`${s})`, {
time: time.end(`step ${s}`),
candidates,
score: model.score
})
if (!candidates) break
}
if (output) {
if (isGIF) {
await context.saveGIF(frames, output, opts)
await rmfr(tempDir)
} else {
await context.saveImage(model.current, output, opts)
}
}
return model
}