|
<!DOCTYPE html> |
|
<html lang="en-GB"> |
|
<head> |
|
<title>HTML5 canvas spray-paint/masking-tape art</title> |
|
<script src="https://unpkg.com/[email protected]"></script> |
|
<script src="https://unpkg.com/d3-jetpack-module"></script> |
|
<meta charset=utf-8> |
|
<style> |
|
body{background: #fff; margin: 20px;} |
|
html, text{font-family: Avenir; font-size: 18px; fill:#43423e; color:#43423e;} |
|
canvas{display: inline-block; cursor: crosshair;} |
|
#canvas{border:1px solid #000; position: absolute;} |
|
#svg{position: absolute; pointer-events: none; overflow:visible;} |
|
#svg.active{pointer-events: all; cursor: crosshair;} |
|
.mask{pointer-events: all; cursor: pointer;} |
|
span{vertical-align: top;} |
|
#diskMask, #tapeMask, #radius{cursor: pointer;} |
|
line,.mask{clip-path:url("#clip");} |
|
</style> |
|
</head> |
|
<body> |
|
<div id=colorPicker> |
|
<span>Hue</span> <canvas id=h></canvas> |
|
<span>Sat.</span> <canvas id=s></canvas> |
|
<span>Lightness</span> <canvas id=l></canvas> |
|
<span style=color:#aaa;>Alpha</span> <canvas id=a></canvas> |
|
</div> |
|
<div id=color> |
|
<span>Paint colour:</span> <canvas id=c></canvas> |
|
<span>Spray radius:</span> <span id=radiusText>100</span> <input id=radius type=range min=10 max=100 step=5 value=100></input> |
|
<span id=diskMask>✚ disk mask</span> |
|
<span id=tapeMask>✚ tape mask</span> |
|
</div> |
|
<div id=radiusDiv> |
|
</div> |
|
<div id=frame> |
|
<canvas id=canvas></canvas> |
|
<svg id=svg></svg> |
|
</div> |
|
<script type="text/javascript" charset="utf-8"> |
|
|
|
let width = 970, |
|
height = 600, |
|
pixRatio = window.devicePixelRatio || 1, |
|
scaledWidth = width*pixRatio, |
|
scaledHeight = height*pixRatio, |
|
radius = 100, |
|
paintColour = "hsla(350, 100%, 58%, 0.3)", |
|
h=350,s=100,l=58,a=1, |
|
masks = [], |
|
polygonData = []; |
|
|
|
function pointInCircle(p, c, r){ |
|
return Math.pow(Math.pow(p.x - c.x,2) + Math.pow(p.y - c.y,2), 0.5) < r; |
|
}; |
|
|
|
function pointInPolygon(px, py, vs) { |
|
// ray-casting algorithm based on |
|
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html |
|
|
|
let inside = false; |
|
for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) { |
|
let xi = vs[i][0], yi = vs[i][1]; |
|
let xj = vs[j][0], yj = vs[j][1]; |
|
|
|
let intersect = ((yi > py) != (yj > py)) |
|
&& (px < (xj - xi) * (py - yi) / (yj - yi) + xi); |
|
if (intersect) inside = !inside; |
|
} |
|
|
|
return inside; |
|
}; |
|
|
|
function getRGBA(x,y){ |
|
return "rgba(" + context.getImageData(x*2, y*2, 1, 1).data.join(",") + ")"; |
|
} |
|
|
|
const path = d3.line() |
|
.x(d => d[0]) |
|
.y(d => d[1]); |
|
|
|
let svg = d3.select("#svg") |
|
.at({ |
|
width: width, |
|
height: height |
|
}) |
|
.st({ |
|
width: width + "px", |
|
height: height + "px" |
|
}); |
|
|
|
let defs = svg.append("defs"); |
|
defs.append("clipPath#clip") |
|
.append("rect") |
|
.at({ |
|
x: 0, |
|
y: 0, |
|
width: width, |
|
height: height |
|
}); |
|
|
|
|
|
function unMask(){ |
|
svg.selectAll(".mask") |
|
.on("mousedown", function(){ |
|
d3.event.stopPropagation(); |
|
}) |
|
.on("dblclick", function(){ |
|
d3.event.stopPropagation(); |
|
let thisMask = d3.select(this); |
|
thisMask.remove(); |
|
masks = masks.filter(d => d.id != thisMask.attr("id")); |
|
}); |
|
} |
|
|
|
function circularMask(cx,cy,r){ |
|
|
|
svg.append("circle.mask") |
|
// .translate([cx, cy]) |
|
.at({ |
|
cx: cx, |
|
cy: cy, |
|
id: "_" + (masks.length+1), |
|
r: r, |
|
fill: "#43423e", |
|
stroke: "#43423e", |
|
"fill-opacity": 0.9 |
|
}); |
|
|
|
masks.push({ |
|
shape: "circle", |
|
id: "_" + (masks.length+1), |
|
x: cx, |
|
y: cy, |
|
r: r |
|
}); |
|
|
|
unMask(); |
|
|
|
} |
|
|
|
function polygonMask(points){ |
|
|
|
svg.append("path.mask") |
|
.at({ |
|
d: path(points), |
|
id: "_" + masks.length+1, |
|
fill: "#43423e", |
|
stroke: "#43423e", |
|
"stroke-width": 4, |
|
opacity: 0.9 |
|
}); |
|
|
|
masks.push({ |
|
shape: "polygon", |
|
id: "_" + masks.length+1, |
|
points: points |
|
}); |
|
|
|
unMask(); |
|
} |
|
|
|
function moveDisk(){ |
|
|
|
let oldCentre, |
|
newCx, |
|
newCy; |
|
|
|
svg.selectAll("circle.mask") |
|
.call( |
|
d3.drag() |
|
.on("start", function(){ |
|
let thisMask = d3.select(this); |
|
oldCentre = [+thisMask.attr("cx"), +thisMask.attr("cy")]; |
|
oldMouse = d3.mouse(svg.node()); |
|
}) |
|
.on("drag", function(){ |
|
let thisMask = d3.select(this); |
|
let newMouse = d3.mouse(svg.node()); |
|
let xMove = newMouse[0]-oldMouse[0], |
|
yMove = newMouse[1]-oldMouse[1]; |
|
newCx = oldCentre[0]+xMove; |
|
newCy = oldCentre[1]+yMove; |
|
d3.select(this) |
|
.at({ |
|
cx: newCx, |
|
cy: newCy |
|
}); |
|
}) |
|
.on("end", function(){ |
|
let thisMask = d3.select(this); |
|
masks.filter(d => d.id == thisMask.attr("id"))[0].x = newCx; |
|
masks.filter(d => d.id == thisMask.attr("id"))[0].y = newCy; |
|
}) |
|
); |
|
} |
|
|
|
function addDisk(){ |
|
|
|
let diskRadius = 5, |
|
diskCentre, |
|
diskCx, |
|
diskCy, |
|
diskDragCentre, |
|
diskDragCx, |
|
diskDragCy, |
|
activeCircle; |
|
|
|
svg.classed("active", 1).call( |
|
d3.drag() |
|
.on("start", function(){ |
|
diskCentre = d3.mouse(svg.node()); |
|
diskCx = diskCentre[0]; |
|
diskCy = diskCentre[1]; |
|
}) |
|
.on("drag", function(){ |
|
activeCircle = svg.selectAll("circle#active") |
|
.data([diskCentre]) |
|
.enter() |
|
.append("circle#active") |
|
// .translate([diskCx, diskCy]) |
|
.at({ |
|
cx: diskCx, |
|
cy: diskCy, |
|
r: diskRadius |
|
}) |
|
|
|
diskDragCentre = d3.mouse(svg.node()), |
|
diskDragCx = diskDragCentre[0], |
|
diskDragCy = diskDragCentre[1]; |
|
diskRadius = Math.pow(Math.pow(diskCx-diskDragCx, 2) + Math.pow(diskCy-diskDragCy, 2), 0.5); |
|
|
|
svg.selectAll("circle#active").at({ |
|
r: diskRadius, |
|
fill: "#43423e", |
|
stroke: "#43423e", |
|
"fill-opacity": 0.9 |
|
}); |
|
}) |
|
.on("end", function(){ |
|
svg.selectAll("circle#active").remove() |
|
circularMask(diskCx, diskCy, diskRadius); |
|
svg.classed("active",0); |
|
moveDisk(); |
|
}) |
|
|
|
) |
|
|
|
} |
|
|
|
function addTape(){ |
|
let tape1, |
|
tapeX1, |
|
tapeY1, |
|
tape2, |
|
tapeX2, |
|
tapeY2, |
|
slope, |
|
intercept, |
|
rayX1, |
|
rayY1, |
|
rayX2, |
|
rayY2, |
|
h, |
|
v; |
|
|
|
svg.classed("active", 1).call( |
|
d3.drag() |
|
.on("start", function(){ |
|
tape1 = d3.mouse(svg.node()); |
|
tapeX1 = tape1[0]; |
|
tapeY1 = tape1[1]; |
|
tape2 = d3.mouse(svg.node()); |
|
tapeX2 = tape2[0]+1; |
|
tapeY2 = tape2[1]+1; |
|
svg.selectAll("line#active") |
|
.data([tape1]) |
|
.enter() |
|
.append("line#active") |
|
.at({ |
|
x1: tapeX1, |
|
y1: tapeY1, |
|
x2: tapeX2, |
|
y2: tapeY2, |
|
stroke: "#43423e", |
|
"stroke-width":24 |
|
}); |
|
|
|
svg.selectAll("line#ray") |
|
.data([tape1]) |
|
.enter() |
|
.append("line#ray") |
|
.at({ |
|
x1: tapeX1, |
|
y1: tapeY1, |
|
x2: tapeX2, |
|
y2: tapeY2, |
|
stroke: "#000", |
|
"stroke-width":2 |
|
}); |
|
|
|
}) |
|
.on("drag", function(){ |
|
tape2 = d3.mouse(svg.node()); |
|
tapeX2 = tape2[0]; |
|
tapeY2 = tape2[1]; |
|
|
|
slope = (tapeY1-tapeY2)/(tapeX1-tapeX2); |
|
intercept = (tapeY2-(tapeX2*slope)); |
|
|
|
if(intercept < 0){ |
|
rayY1 = 0; |
|
rayX1 = -intercept/slope; |
|
if(slope*width+intercept > height){ |
|
rayY2 = height; |
|
rayX2 = (rayY2-intercept)/slope; |
|
}else{ |
|
rayX2 = width; |
|
rayY2 = (slope*rayX2)+intercept; |
|
} |
|
}else if(intercept > height){ |
|
rayY1 = height; |
|
rayX1 = (rayY1-intercept)/slope; |
|
if(slope*width+intercept < 0){ |
|
rayY2 = 0; |
|
rayX2 = -intercept/slope; |
|
}else{ |
|
rayX2 = width; |
|
rayY2 = (slope*rayX2)+intercept; |
|
} |
|
}else{ |
|
rayX1 = 0; |
|
rayY1 = intercept; |
|
if(slope*width+intercept < 0){ |
|
rayY2 = 0; |
|
rayX2 = -intercept/slope; |
|
}else if(slope*width+intercept > height){ |
|
rayY2 = height; |
|
rayX2 = (rayY2-intercept)/slope; |
|
}else{ |
|
rayX2 = width; |
|
rayY2 = (slope*rayX2)+intercept; |
|
} |
|
} |
|
|
|
svg.selectAll("line#active") |
|
.at({ |
|
x1: tapeX1, |
|
y1: tapeY1, |
|
x2: tapeX2, |
|
y2: tapeY2 |
|
}); |
|
|
|
svg.selectAll("line#ray") |
|
.at({ |
|
x1: rayX1, |
|
y1: rayY1, |
|
x2: rayX2, |
|
y2: rayY2 |
|
}); |
|
}) |
|
.on("end", function(){ |
|
svg.selectAll("line").remove(); |
|
|
|
h = Math.abs(12 / Math.cos((Math.PI/180) * (90/(Math.abs(slope)+1)))); |
|
v = Math.abs(12 / Math.cos((Math.PI/180) * (90-90/(Math.abs(slope)+1)))); |
|
|
|
if(slope > 0){ |
|
polygonMask([ [rayX1, rayY1-v], [rayX2+h, rayY2], [rayX2+h, rayY2+2*v], [rayX1-2*h, rayY1-v] ]); |
|
}else{ |
|
polygonMask([ [rayX1, rayY1+v], [rayX2+2*h, rayY2-v], [rayX2, rayY2-v], [rayX1-h, rayY1] ]); |
|
} |
|
|
|
svg.classed("active",0); |
|
}) |
|
) |
|
|
|
} |
|
|
|
function addPoint(){ |
|
|
|
let pointIndex, |
|
pointCentre, |
|
pointCx, |
|
pointCy, |
|
activePoint; |
|
|
|
svg.classed("active", 1).call( |
|
d3.drag() |
|
.on("start", function(){ |
|
|
|
pointIndex = polygonData.length; |
|
|
|
pointCentre = d3.mouse(svg.node()); |
|
pointCentre.push(pointIndex); |
|
polygonData[pointIndex] = pointCentre; |
|
|
|
}) |
|
.on("drag", function(){ |
|
|
|
activePoint = svg.selectAll("circle#active") |
|
.data(polygonData) |
|
.enter() |
|
.append("circle#active") |
|
.translate(d => [d[0], d[1]]) |
|
.at({ |
|
r: 5, |
|
fill: "red" |
|
}); |
|
|
|
pointCentre = d3.mouse(svg.node()); |
|
pointCentre.push(pointIndex); |
|
polygonData[pointIndex] = pointCentre; |
|
|
|
svg.selectAll("circle#active") |
|
.translate(d => [d[0], d[1]]); |
|
}) |
|
// .on("end", function(){ |
|
// svg.selectAll("circle#active").remove(); |
|
// polygonMask(polygonData); |
|
// svg.classed("active",0); |
|
// }) |
|
|
|
) |
|
|
|
} |
|
|
|
d3.select("#diskMask").on("click", addDisk); |
|
|
|
d3.select("#tapeMask").on("click", addTape); |
|
|
|
function pickColor(){ |
|
|
|
colContext.clearRect(0,0,100,40); |
|
// paintColour = `hsla(${h},${s}%,${l}%,${a})`; |
|
paintColour = `hsla(${h},${s}%,${l}%,0.3)`; |
|
colContext.fillStyle = paintColour; |
|
colContext.beginPath(); |
|
colContext.rect(0,0,100,40); |
|
colContext.fill(); |
|
colContext.closePath(); |
|
|
|
hueContext.clearRect(0,0,180,40); |
|
d3.range(0,360).forEach(d => { |
|
hueContext.fillStyle = `hsla(${d},${s}%,${l}%,${a})`; |
|
hueContext.beginPath(); |
|
hueContext.rect(d/2,0,1,40); |
|
hueContext.fill(); |
|
hueContext.closePath(); |
|
}); |
|
|
|
satContext.clearRect(0,0,180,40); |
|
d3.range(0,100).forEach(d => { |
|
satContext.fillStyle = `hsla(${h},${d}%,${l}%,${a})`; |
|
satContext.beginPath(); |
|
satContext.rect(d*4,0,4,40); |
|
satContext.fill(); |
|
satContext.closePath(); |
|
}); |
|
|
|
ligContext.clearRect(0,0,180,40); |
|
d3.range(0,100).forEach(d => { |
|
ligContext.fillStyle = `hsla(${h},${s}%,${d}%,${a})`; |
|
ligContext.beginPath(); |
|
ligContext.rect(d*4,0,4,40); |
|
ligContext.fill(); |
|
ligContext.closePath(); |
|
}); |
|
|
|
|
|
alpContext.clearRect(0,0,180,40); |
|
d3.range(0,1,0.01).forEach(d => { |
|
alpContext.fillStyle = `hsla(${h},${s}%,${l}%,${d})`; |
|
alpContext.beginPath(); |
|
alpContext.rect(d*400,0,4,40); |
|
alpContext.fill(); |
|
alpContext.closePath(); |
|
}); |
|
} |
|
|
|
d3.select("#radius") |
|
.on("change", function(){ |
|
radius = +d3.select(this).node().value; |
|
|
|
d3.select("#radiusText").html(radius); |
|
|
|
outerCircle = d3.range(0, radius*radius*16); |
|
|
|
outerCircle.forEach(function(i){ |
|
outerCircle[i] = {x: i % (radius*4)/2, y: Math.ceil(i/(radius*4))/2} |
|
}); |
|
|
|
outerCircle = outerCircle |
|
.filter(d => innerCircle.indexOf(d) <= 0) |
|
.filter(d => pointInCircle(d, {x:radius, y:radius}, radius)); |
|
}); |
|
|
|
const hue = d3.select("#h") |
|
.at({ |
|
width: 360, |
|
height: 40, |
|
}) |
|
.st({ |
|
width: "180px", |
|
height: "20px" |
|
}); |
|
|
|
const hueContext = hue.node().getContext("2d"); |
|
|
|
hueContext.scale(pixRatio, pixRatio); |
|
|
|
hue.call( |
|
d3.drag() |
|
.on("drag", function(){ |
|
h = d3.mouse(hue.node())[0]*2; |
|
pickColor(); |
|
}) |
|
) |
|
.on("click", function(){ |
|
h = d3.mouse(hue.node())[0]*2; |
|
pickColor(); |
|
}); |
|
|
|
d3.range(0,360).forEach(d => { |
|
hueContext.fillStyle = `hsla(${d},${s}%,${l}%,${a})`; |
|
hueContext.beginPath(); |
|
hueContext.rect(d/2,0,1,40); |
|
hueContext.fill(); |
|
hueContext.closePath(); |
|
}); |
|
|
|
|
|
|
|
const sat = d3.select("#s") |
|
.at({ |
|
width: 360, |
|
height: 40, |
|
}) |
|
.st({ |
|
width: "180px", |
|
height: "20px" |
|
}); |
|
|
|
const satContext = sat.node().getContext("2d"); |
|
|
|
satContext.scale(pixRatio, pixRatio); |
|
|
|
sat.call( |
|
d3.drag() |
|
.on("drag", function(){ |
|
s = d3.mouse(sat.node())[0]/1.8; |
|
pickColor(); |
|
}) |
|
) |
|
.on("click", function(){ |
|
s = d3.mouse(sat.node())[0]/1.8; |
|
pickColor(); |
|
}); |
|
|
|
d3.range(0,100).forEach(d => { |
|
satContext.fillStyle = `hsla(${h},${d}%,${l}%,${a})`; |
|
satContext.beginPath(); |
|
satContext.rect(d*4,0,4,40); |
|
satContext.fill(); |
|
satContext.closePath(); |
|
}); |
|
|
|
|
|
|
|
const lig = d3.select("#l") |
|
.at({ |
|
width: 360, |
|
height: 40, |
|
}) |
|
.st({ |
|
width: "180px", |
|
height: "20px" |
|
}); |
|
|
|
const ligContext = lig.node().getContext("2d"); |
|
|
|
ligContext.scale(pixRatio, pixRatio); |
|
|
|
lig.call( |
|
d3.drag() |
|
.on("drag", function(){ |
|
l = d3.mouse(lig.node())[0]/1.8; |
|
pickColor(); |
|
}) |
|
) |
|
.on("click", function(){ |
|
l = d3.mouse(lig.node())[0]/1.8; |
|
pickColor(); |
|
}); |
|
|
|
d3.range(0,100).forEach(d => { |
|
ligContext.fillStyle = `hsla(${h},${s}%,${d}%,${a})`; |
|
ligContext.beginPath(); |
|
ligContext.rect(d*4,0,4,40); |
|
ligContext.fill(); |
|
ligContext.closePath(); |
|
}); |
|
|
|
|
|
|
|
|
|
const alp = d3.select("#a") |
|
.at({ |
|
width: 360, |
|
height: 40, |
|
}) |
|
.st({ |
|
width: "180px", |
|
height: "20px" |
|
}); |
|
|
|
const alpContext = alp.node().getContext("2d"); |
|
|
|
alpContext.scale(pixRatio, pixRatio); |
|
|
|
alp.call( |
|
d3.drag() |
|
.on("drag", function(){ |
|
a = d3.mouse(alp.node())[0]/180; |
|
pickColor(); |
|
}) |
|
) |
|
.on("click", function(){ |
|
a = d3.mouse(alp.node())[0]/180; |
|
pickColor(); |
|
}); |
|
|
|
d3.range(0,1,0.01).forEach(d => { |
|
alpContext.fillStyle = `hsla(${h},${s}%,${l}%,${d})`; |
|
alpContext.beginPath(); |
|
alpContext.rect(d*400,0,4,40); |
|
alpContext.fill(); |
|
alpContext.closePath(); |
|
}); |
|
|
|
const col = d3.select("#c") |
|
.at({ |
|
width: 100, |
|
height: 40, |
|
}) |
|
.st({ |
|
width: "50px", |
|
height: "20px" |
|
}); |
|
|
|
const colContext = col.node().getContext("2d"); |
|
|
|
colContext.scale(pixRatio, pixRatio); |
|
|
|
colContext.fillStyle = "hsla(350, 100%, 58%, 1)"; |
|
colContext.beginPath(); |
|
colContext.rect(0,0,100,40); |
|
colContext.fill(); |
|
colContext.closePath(); |
|
|
|
|
|
|
|
let canvas = d3.select("#canvas") |
|
.at({ |
|
width: scaledWidth, |
|
height: scaledHeight |
|
}) |
|
.st({ |
|
width: width + "px", |
|
height: height + "px" |
|
}); |
|
|
|
let context = canvas.node().getContext("2d"); |
|
|
|
context.scale(pixRatio, pixRatio); |
|
|
|
context.save(); |
|
|
|
context.fillStyle = "rgba(255,255,255,1)"; |
|
// context.fillStyle = "rgba(0,0,0,1)"; |
|
|
|
context.beginPath(); |
|
context.rect(0,0,scaledWidth,scaledHeight); |
|
context.fill(); |
|
context.closePath(); |
|
|
|
let innerCircle = d3.range(0, 0); |
|
|
|
innerCircle.forEach(function(i){ |
|
innerCircle[i] = {x: i % (radius) + radius/2, y: Math.ceil(i/(radius)) + radius/2} |
|
}); |
|
|
|
innerCircle = innerCircle.filter(d => pointInCircle(d, {x:radius, y:radius}, radius/2)); |
|
|
|
let outerCircle = d3.range(0, radius*radius*16); |
|
|
|
outerCircle.forEach(function(i){ |
|
outerCircle[i] = {x: i % (radius*4)/2, y: Math.ceil(i/(radius*4))/2} |
|
}); |
|
|
|
outerCircle = outerCircle |
|
.filter(d => innerCircle.indexOf(d) <= 0) |
|
.filter(d => pointInCircle(d, {x:radius, y:radius}, radius)); |
|
|
|
context.fillStyle = paintColour; |
|
|
|
function spray(xy){ |
|
|
|
let pointsInCircle = innerCircle |
|
.filter(function(){return Math.random() > 0.75}) |
|
.concat(outerCircle.filter(function(){return Math.random() > 0.97})); |
|
|
|
masks.forEach(function(m){ |
|
if(m.shape == "circle"){ |
|
pointsInCircle = pointsInCircle |
|
.filter(function(d){ |
|
let circle = {x:xy.x + d.x - radius, y: xy.y + d.y - radius}; |
|
return !pointInCircle(circle, {x:m.x, y:m.y}, m.r) |
|
}); |
|
}else if(m.shape == "polygon"){ |
|
pointsInCircle = pointsInCircle |
|
.filter(function(d){ |
|
let circle = {x:xy.x + d.x - radius, y: xy.y + d.y - radius}; |
|
return !pointInPolygon(circle.x, circle.y, m.points) |
|
}); |
|
} |
|
}) |
|
|
|
pointsInCircle.forEach(d => { |
|
context.fillStyle = paintColour; |
|
context.fillRect(xy.x + d.x - radius,xy.y + d.y - radius,0.5,0.5); |
|
}); |
|
|
|
context.restore(); |
|
}; |
|
|
|
canvas.call( |
|
d3.drag() |
|
.on("drag", function(){ |
|
let xy = d3.mouse(canvas.node()), |
|
_x = xy[0], |
|
_y = xy[1]; |
|
spray({x: _x, y: _y}); |
|
}) |
|
); |
|
|
|
</script> |
|
</body> |
|
</html> |