Skip to content

Commit

Permalink
updated Readme
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurmasson committed May 2, 2024
1 parent e880cf0 commit bbe7b76
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 114 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ A sort of artwork

Drag'n'drop the image of your choice on the canvas to generate a hilbert or gosper curve version.

## Usage

Choose a curve type, drag-and-drop an image, choose some thresholds, and it will generate a curve. You can then export it with "exportJSON" and then use `jsonToSVG.py` to create an SVG drawing.

Only Gosper curve can be colored.
There is one threshold per iteration / level. The global threshold sets all other thresholds to the same value, it's just a shortcut.

The lightness parameter enable to reduce the amount of black when in color mode. It does not have any effect in black and white mode.

You can choose the scale and position of the image, and hide / show the image which will be drawn.

### Convert JSON to SVG

Use `python jsonToSVG.py -i path/to/drawing.json` to create the corresponding `drawing.svg`. You can add a small margin around the drawing with the `--margin` option (in percentage of the max side of the drawing bounding box).

## How does it work?

A [Space Filling Curve](https://en.wikipedia.org/wiki/Space-filling_curve) ([Hilbert curve](https://en.wikipedia.org/wiki/Hilbert_curve) or a [Gosper curve](https://en.wikipedia.org/wiki/Gosper_curve)) is computed from a grayscale image, refined where the image is darker than thredhold.
Expand Down
65 changes: 33 additions & 32 deletions jsonToSVG.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@
parser = argparse.ArgumentParser('JSON to SVG', description='Converts a json file generated with the space-filling-curve webpage to svg.', formatter_class=argparse.ArgumentDefaultsHelpFormatter)

parser.add_argument('-i', '--input', help='Input json', required=True)
parser.add_argument('-s', '--sorted', action='store_true', help='Path is sorted. Use to skip points sorting.')
# parser.add_argument('-s', '--sorted', action='store_true', help='Path is sorted. Use to skip points sorting.')
parser.add_argument('-m', '--margin', type=float, help='Margin (defined as a percentage of the max side of the drawing bounding box)', default=0.01)

args = parser.parse_args()

with open(args.input, 'r') as f:
data = json.load(f)

fdata = data if args.sorted else [p for path in data for p in path]
# fdata = data if args.sorted else [p for path in data for p in path]
fdata = [p for path in data for p in path]

xs = [p[0] for p in fdata]
ys = [p[1] for p in fdata]
Expand All @@ -29,36 +30,36 @@
def areClose(p1, p2):
return abs(p2[0] - p1[0]) < 1e-6 and abs(p2[1] - p1[1]) < 1e-6

if not args.sorted:
width = maxX - minX
height = maxY - minY

# tree = quads.QuadTree((minX+width/2, minY+height/2), 2*width, 2*height)
ps = [[path[0], path[-1]] for path in data]
ps = [p for s in ps for p in s]

print('Creating tree...')
tree = KDTree(ps)

# for d in data:
# tree.insert(quads.Point(d[0][0], d[0][1]), data=(d, True))
# tree.insert(quads.Point(d[-1][0], d[-1][1]), data=(d, False))
print('Creating path...')
finalData = data[0].copy()
n = 1
addedIndices = set()
addedIndices.add(0)
while n<len(data):
# (d, inverted) = tree.find(finalData[-1]).data
dd, ii = tree.query(finalData[-1], k=2)
index = ii[0]//2 if ii[0]//2 not in addedIndices else ii[1]//2
addedIndices.add(index)
d = data[index]
inverted = areClose(finalData[-1], d[-1])
finalData += d[::-1][1:] if inverted else d[1:]
n += 1
print('Created path.')
data = finalData
# if not args.sorted:
width = maxX - minX
height = maxY - minY

# tree = quads.QuadTree((minX+width/2, minY+height/2), 2*width, 2*height)
ps = [[path[0], path[-1]] for path in data]
ps = [p for s in ps for p in s]

print('Creating tree...')
tree = KDTree(ps)

# for d in data:
# tree.insert(quads.Point(d[0][0], d[0][1]), data=(d, True))
# tree.insert(quads.Point(d[-1][0], d[-1][1]), data=(d, False))
print('Creating path...')
finalData = data[0].copy()
n = 1
addedIndices = set()
addedIndices.add(0)
while n<len(data):
# (d, inverted) = tree.find(finalData[-1]).data
dd, ii = tree.query(finalData[-1], k=2)
index = ii[0]//2 if ii[0]//2 not in addedIndices else ii[1]//2
addedIndices.add(index)
d = data[index]
inverted = areClose(finalData[-1], d[-1])
finalData += d[::-1][1:] if inverted else d[1:]
n += 1
print('Created path.')
data = finalData


svgName = Path(args.input).with_suffix('.svg')
Expand Down
106 changes: 24 additions & 82 deletions main.js
Original file line number Diff line number Diff line change
@@ -1,71 +1,29 @@
let parameters = {
nIterations: 8,
globalThreshold: 0.03,
// margin: 0.0, // In proportion of the size of the squared image (length of one side)
scale: 1.0,
posX: 0.0,
posY: 0.0,
lightness: 0.5,
showImage: true,
color: false,
sortPath: false,
type: 'hilbert',
exportJSON: ()=> {
let visible = preview.visible;
preview.visible = false;
let path = []
if(parameters.sortPath) {
let cs = compoundPath.children.slice()
let firstChild = cs.shift()
let segments = firstChild.segments
path.push([segments[0].point.x, segments[0].point.y])
while(cs.length>0) {
for(let i=1 ; i<segments.length ; i++) {
path.push([segments[i].point.x, segments[i].point.y])
}
for(let i=0 ; i<cs.length ; i++) {
let child = cs[i]
if(child.firstSegment.point.isClose(path[path.length-1], 1e-6)) {
segments = child.segments
cs.splice(i, 1)
break
}
if(child.lastSegment.point.isClose(path[path.length-1], 1e-6)) {
segments = child.segments.toReversed()
cs.splice(i, 1)
break
}
}
}
} else {

for(let child of compoundPath.children) {
let p = []
for(let segment of child.segments) {
p.push([segment.point.x, segment.point.y])
}
path.push(p)

for(let child of compoundPath.children) {
let p = []
for(let segment of child.segments) {
p.push([segment.point.x, segment.point.y])
}
path.push(p)
}

// let n=1
// for(let p of path) {
// var text = new paper.PointText({
// point: p,
// content: ''+n,
// fillColor: 'black',
// fontFamily: 'Courier New',
// fontSize: 8
// });
// n++
// }


let blob = new Blob([JSON.stringify(path)], {type: "application/json"})
let url = URL.createObjectURL(blob)

// let svg = paper.project.exportJSON( { asString: true });
// // create an svg image, create a link to download the image, and click it
// let blob = new Blob([svg], {type: 'image/svg+xml'});
// let url = URL.createObjectURL(blob);
let link = document.createElement("a");
document.body.appendChild(link);
link.download = 'indian.json';
Expand Down Expand Up @@ -119,11 +77,8 @@ raster.on('load', rasterLoaded);
let preview = null;
let generatingText = null;

// let compoundPath = new paper.CompoundPath();
let compoundPath = new paper.Layer();
let topLayer = new paper.Layer();
// compoundPath.strokeWidth = 0.5;
// compoundPath.strokeColor = 'black';

function gosper(rasters, nIterations, i, p1, p2, invert, container, channel='gray') {
n = nIterations - i
Expand All @@ -138,13 +93,6 @@ function gosper(rasters, nIterations, i, p1, p2, invert, container, channel='gra
let imageSize = rasters[n - 1].width
let centerImage = center.multiply(imageSize / container.width).floor()

// console.log('containerSize: ', containerSize)
// console.log('imageSize: ', imageSize, rasters[n-1].height)
// console.log('p1: ', p1)
// console.log('p2: ', p2)
// console.log('center: ', center)
// console.log('centerImage: ', centerImage)

let direction = new paper.Point(2.5, Math.sqrt(3) / 2)
let step = p1p2Length / direction.length
let angle = direction.angle
Expand All @@ -164,20 +112,14 @@ function gosper(rasters, nIterations, i, p1, p2, invert, container, channel='gra

let raster = rasters[n - 1]
let color = raster.getAverageColor(new paper.Path.Circle(raster.bounds.topLeft.add(centerImage), 1.5))
// let gray = color != null ? color.gray : -1
let gray = color != null ? rgb_to_cmyk(color.red, color.green, color.blue)[channel] : -1

// if(Math.random()<0.01) {
// console.log(channel, gray)
// }

// if(1 - gray >= parameters.threshold) {
let gray = color != null ? rgb_to_cmyk(color.red, color.green, color.blue)[channel] : -1

// if((nIterations-n) < 4.0 * parameters.threshold * (1-gray) * nIterations) {
// if( gray < parameters.threshold * n / nIterations) {
// if( 1 - gray < parameters.threshold * n / nIterations ) {
if(channel == 'black' && parameters.color) {
gray *= parameters.lightness
}

if( 1 - gray < parameters['threshold'+n] ) {
// if( 1-gray < parameters.threshold ) {

for(let j=0 ; j<deltas.length-1 ; j++) {
let invert = deltas[j].invert
Expand All @@ -198,11 +140,6 @@ function gosper(rasters, nIterations, i, p1, p2, invert, container, channel='gra
path.add(d.point)
}

// let circle = new paper.Path.Circle(deltas[deltas.length-1].point, n*3)
// circle.strokeWidth = 0.8
// circle.strokeColor = 'black'
// circle.fillColor = null
// compoundPath.addChild(circle)
}

function hilbert(rasters, nIterations, i, x, y, px, py, quadrant, childNumber, rotation, oppositeDirection, inversion, margin, size) {
Expand Down Expand Up @@ -353,9 +290,7 @@ function draw() {
let p4 = new paper.Point(maxContainerSize * (1 + Math.sqrt(3) / 2) / 2, 1 * maxContainerSize / 4)
let p5 = new paper.Point(maxContainerSize / 2, 0)
let p6 = new paper.Point(maxContainerSize * (1 - Math.sqrt(3) / 2) / 2, 1 * maxContainerSize / 4)
// let p3 = new paper.Point(p1.x, p2.y)
// let p4 = new paper.Point(p2.x, p1.y)
// gosper(rasters, nIterations, 1, p1, p2, false, container);

if(parameters.color) {
gosper(rasters, nIterations, 1, p1, p3, false, container, 'cyan');
gosper(rasters, nIterations, 1, p3, p5, false, container, 'magenta');
Expand Down Expand Up @@ -420,20 +355,28 @@ var gui = new dat.GUI();
gui.add(parameters, 'type', ['hilbert', 'gosper']).onFinishChange((value)=> {
if(value == 'hilbert') {
parameters.nIterations = 9
globalThresholdController.setValue(0.03)
parameters.globalThreshold = 0.03
parameters.margin = 0
} else if(value == 'gosper') {
parameters.nIterations = 8
globalThresholdController.setValue(0.02455)
parameters.globalThreshold = 0.85
parameters.margin = 0.2
}
globalThresholdController.setValue(parameters.globalThreshold)
for(let n=0 ; n<nThresholds ; n++) {
parameters['threshold'+n] = parameters.globalThreshold
thresholdControllers[n].setValue(parameters.globalThreshold)
}
gui.updateDisplay()

displayGeneratingAndDraw();
});
gui.add(parameters, 'color').onFinishChange(()=> {
displayGeneratingAndDraw();
});
gui.add(parameters, 'lightness', 0, 1, 0.01).onFinishChange(()=> {
displayGeneratingAndDraw();
});

gui.add(parameters, 'nIterations', 1, nThresholds, 1).onFinishChange(()=> {
displayGeneratingAndDraw();
Expand Down Expand Up @@ -466,5 +409,4 @@ gui.add(parameters, 'showImage').onFinishChange((value)=> {
preview.visible = value;
}
});
gui.add(parameters, 'sortPath');
gui.add(parameters, 'exportJSON');

0 comments on commit bbe7b76

Please sign in to comment.