HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by Skypack, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ES6 import
usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css" />
</head>
<body>
<div style="padding:10px">
<div id="topHeading">
<i class="fas fa-th"></i>
<h2 style="display:inline;">Grid Jr.</h2>
</div>
<i class="fa fa-pause pause" onclick="pauseSong(this)" style="cursor:pointer;"></i>
<input type="file" id="songDropdown" onchange="songChanged()" />
or stream via <a href="https://www.internet-radio.com/" target="_blank">internet-radio.com</a>:
<select id="radioStations">
<option value="" selected>Select a station</option>
<option value="https://cors-anywhere.herokuapp.com/http://hirschmilch.de:7000/;stream/3">Hirschmilch.de </option>
<option value="https://cors-anywhere.herokuapp.com/http://174.36.206.197:8000/;stream/1">VeniceClassicRadio.eu</option>
<option value="https://cors-anywhere.herokuapp.com/http://us4.internet-radio.com:8266/;stream">SmoothJazzFlorida.com</option>
<option value="https://cors-anywhere.herokuapp.com/http://us4.internet-radio.com:8258/;stream">ClassicRockFlorida.com</option>
<!--<option value="https://cors-anywhere.herokuapp.com/http://uk7.internet-radio.com:8226/;stream">DanceRadioUK.com</option>-->
</select>
</div>
<div><label for="ctl_presets">Preset:</label> <select id="ctl_presets" data-option="preset"></select></div>
<div id="canvasContainer">
<canvas id="scope"></canvas>
</div>
html,
body,
div,
canvas {
margin: 0;
padding: 0;
}
body {
padding: 10px;
height: 100vh;
background-color: black;
color: chartreuse;
font-family: monospace;
text-align: center;
}
input[type="file"] {
border: 1px solid gray;
}
#topHeading {
padding: 5px;
}
#canvasContainer {
display: flex;
flex-direction: column;
height: 90%;
min-width: 300px;
min-height: 300px;
}
@media (max-width: 750px) {
body {
font-size: 90%;
}
}
(function () {
/* INIT PHASE */
/*AUDIO context and related properties*/
var AudioContext = window.AudioContext || window.webkitAudioContext
var audioContext
var playing = false
var file
var pauseButton
var analyser
var Coord
var frequencies, waveforms
const MAXBYTE = 255
const HALFBYTE = 128
const PI2 = Math.PI * 2
/*GRAPHICS context, sizingInfo properties*/
var body
var container = document.querySelector("#canvasContainer")
var clientWidth, clientHeight
var offsetX, offsetY
var cx_FPS, cy_FPS
var ORIGIN
const smallScreen = 750
var growthFactor
var factor; //minimum window dimension
var canvas = document.getElementById("scope");
var ctx = canvas.getContext('2d');
setSizingInfo()
ctx.lineWidth = factor > 1200 ? 9 : (factor > 1000 ? 7 : (factor > 750 ? 5 : (factor > 640 ? 4 : (
factor > 500 ? 3 : (factor > 360 ? 2 : 1)))))
//DJ KNOBS' SECTION
var controlDefaults = {
pointStyle: "stroke",
pointColor: 360,
pointRadius: .1,
pointTrigger: .80,
multiplier: .2,
centerEffects: "none",
centerData: "waveforms",
dataset: "waveforms",
centerMultiplier: .2,
centerRadius: .2,
centerColor: 360,
fftSize: 1024,
gridSize: 5,
gridPoints: true,
endPoints: true,
paintAlpha: .03,
paintStroke: 1,
backgroundColor: "#000000",
defaultWindow: "weaver",
paintMode: false,
lineWidth: ctx.lineWidth,
initialLineWidth: ctx.lineWidth,
shapeColorFunc: "triad2",
gridPointsColorFunc:"tetrad",
centerColorFunc:"burst1"
}
var controls = {
update: function (config) {
for (const key in config) {
controls[key] = config[key]
}
},
themes: new Themes(),
preset: {
value: "PsychedelicBoombox",
change: function (e) {
controls.preset.value = e.target.value
updatePreset(controls.preset.value)
}
},
radioStations:{
value:"",
change:function(e){
autoPlay(e.target.value)
}
}
}
initControls()
/***** END INITIALIZATION PHASE. POWER UP THE BASS CANNON ******/
function updatePreset(themeKey) {
let theme = controls.themes.find(t => t.key === themeKey)
if (theme.fftSize === undefined) {
theme.fftSize = controlDefaults.fftSize
}
//fftSize_change:
if (analyser) {
analyser.fftSize = theme.fftSize
frequencies = new Uint8Array(analyser.frequencyBinCount)
waveforms = new Uint8Array(analyser.fftSize)
}
controls.update(controlDefaults)
controls.update(theme)
controls.styledShapes = theme.styledShapes
//additional DEFAULT control settings to the styled shapes:
controls.styledShapes.forEach(ss => {
if (ss.multiplier === undefined) ss.multiplier = controlDefaults.multiplier
if (ss.dataset === undefined) ss.dataset = controlDefaults.dataset
if (ss.windowFunc === undefined) ss.windowFunc = controlDefaults.defaultWindow
})
Coord = new CartesianCoord({
width: canvas.width,
height: canvas.height,
gridSize: theme.gridSize || controlDefaults.gridSize,
offsetX: offsetX,
offsetY: offsetY,
themes: controls.themes,
controlDefaults: controlDefaults
})
Coord.points.forEach(p => {
p.hide = undefined
p.endpoint = false
})
controls.styledShapes.forEach(styledShape => {
Coord.markPoints(styledShape.shapeKey)
})
if (theme.lineWidthMultiplier) {
ctx.lineWidth = ctx.lineWidth * theme.lineWidthMultiplier
controls.lineWidth = ctx.lineWidth
} else {
ctx.lineWidth = controls.initialLineWidth
controls.lineWidth = controls.initialLineWidth
}
setBackgroundColorsFromConfig()
}
function initControls() {
updatePreset(controls.preset.value)
let presets = document.querySelector("#ctl_presets")
controls.themes.forEach(theme => {
//fill in the theme <options>
let el = document.createElement("option")
let text = theme.name
el.value = theme.key
el.append(text)
presets.appendChild(el)
document.getElementById("ctl_presets").addEventListener("change", controls.preset.change)
document.getElementById("radioStations").addEventListener("change", controls.radioStations.change)
})
setBackgroundColorsFromConfig()
ctx.font = `${factor>smallScreen?150:100}% monospace`
ctx.fillStyle = controls.color
ctx.textAlign = "center"
ctx.fillText('Please select a music file or radio station', ORIGIN.x, ORIGIN.y)
}
/*** MAIN method ***/
var analyzeTrack = (audioContext, srcNode) => {
analyser = audioContext.createAnalyser();
analyser.fftSize = controls.fftSize
analyser.connect(audioContext.destination);
srcNode.connect(analyser);
//fftSize/2 = frequency bin count
frequencies = new Uint8Array(analyser.frequencyBinCount);
waveforms = new Uint8Array(analyser.fftSize);
/*~~~~THE LOOP~~~~*/
//var i = 0;
//var fps = 0
//var t0, t1
const loop = (time) => {
//t0 = performance.now();
requestAnimationFrame(loop)
if (playing) {
analyser.getByteTimeDomainData(waveforms)
analyser.getByteFrequencyData(frequencies)
if (!controls.paintMode) ctx.clearRect(0, 0, canvas.width, canvas.height)
//loop init variables
const MAX = Math.max(...waveforms)
growthFactor = MAX / MAXBYTE
//dumb, move this out of the loop
let dataMap = {
"waveforms": waveforms,
"frequencies": frequencies,
}
if (controls.pointStyle === "stroke") drawGridPoints({
data: dataMap[controls.centerData]
})
controls.styledShapes.forEach((styled) => {
drawShape({
lines: Coord.getShape(styled.shapeKey).lines,
data: dataMap[styled.dataset],
color: styled.color,
multiplier: styled.multiplier,
windowFunc: windowFunctions[styled.windowFunc || controlDefaults.defaultWindow].value,
lineMode: styled.lineMode
})
})
if (controls.pointStyle === "fill") drawGridPoints({
data: dataMap[controls.centerData]
})
// if (i % 60 == 0) {
// t1 = performance.now()
// fps = Math.floor(1 / ((t1 - t0) / 1000))
// //console.log(`requestAnimFrame took ${t1-t0} ms.`)
// }
}
//i++
}
//srcNode.playbackRate.value = 1
if(srcNode instanceof MediaElementAudioSourceNode){
srcNode.created = true
} else {
srcNode.start()
}
playing = true
loop()
return srcNode
}
/*** END MAIN ***/
/* DRAWING functions */
function drawShape(options) {
//shape, data, valuefunc, color
var data = options.data
var lines = options.lines
var color = options.color
if (controls.shapeColorFunc !== "flat") {
var tiny = tinycolor(color)
var complement = tiny.spin(180).toHexString()
var triad = tiny.spin(120).toHexString()
var tetrad = tiny.spin(90).toHexString()
var analog = tiny.spin(30).toHexString()
var twin = tiny.spin(10).toHexString()
switch (controls.shapeColorFunc) {
case "complement":
color = growthFactor > .9 ? complement : growthFactor > .8 ? triad : growthFactor > .7 ? tetrad : growthFactor > .6 ? analog : color
break
case "triad4":
color = growthFactor > .85 ? triad : growthFactor > .75 ? tetrad : growthFactor > .65 ? analog : color
break
case "triad2":
color = growthFactor > .85 ? triad : growthFactor > .8? analog : color
break
case "analog":
color = growthFactor > .85 ? analog : color
break
case "twin":
color = growthFactor > .8 ? color : twin
break
}
}
var multiplier = (options.multiplier === undefined ? 1 : options.multiplier) * -1 //reversing canvas coords y axis
var windowFunc = options.windowFunc
var lineMode = options.lineMode || "lines"
var valueFunc
function waveValue(byteValue, availableWidth, index, multiplier) {
let value = ((byteValue - HALFBYTE) / MAXBYTE * (availableWidth)) * multiplier
return (value * windowFunc(index, analyser.fftSize))
}
var freqValue = (byteValue, availableWidth, index, multiplier) => {
let value = (byteValue / MAXBYTE * (availableWidth)) * multiplier
return (value * windowFunc(index, analyser.frequencyBinCount))
}
var axisValue = (i, n, totalWidth) => {
return (i + 1) / n * (totalWidth)
}
if (options.data === waveforms) {
valueFunc = waveValue
} else {
valueFunc = freqValue
}
lines.forEach((line => {
if (line.inverted) {
var invertedValue = (b, w, i, m) => {
return -1 * valueFunc(b, w, i, m)
}
}
drawLine({
p1: line.p1,
p2: line.p2,
value: invertedValue || valueFunc
})
}))
function drawLine(options) {
//p1, p2, data, valueFunc, color, multiplier
var p1 = options.p1
var p2 = options.p2
const rise = p2.y - p1.y
const run = p2.x - p1.x
const m = rise / run
const length = Math.sqrt(Math.pow(run, 2) + Math.pow(rise, 2))
const angle = Math.atan(m)
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.save()
ctx.translate(p1.x, p1.y)
ctx.rotate(angle)
ctx.translate(-p1.x, -p1.y)
ctx.beginPath()
ctx.moveTo(p1.x, p1.y)
var lw = ctx.lineWidth
data.forEach((w, i) => {
let deltaX = axisValue(i, data.length, length)
if (run < 0) deltaX = -deltaX
let x = deltaX + p1.x
let y = options.value(w, canvas.height, i, multiplier) + p1.y
if (lineMode === "bars") {
ctx.moveTo(x, p1.y)
ctx.lineTo(x, y)
} else if (lineMode === "dots") {
ctx.fillRect(x, y, lw, lw)
} else { //lines
ctx.lineTo(x, y)
}
})
ctx.stroke()
ctx.restore()
}
}
var drawGridPoints = (options) => {
if (controls.centerEffects === "none" && !controls.gridPoints && !controls.endPoints) return
let data = options.data
//grid points
const pointRadius = controls.pointRadius //% of line
const pointStyle = controls.pointStyle
const peak = controls.pointTrigger
//center point
let centerMultiplier = controls.centerMultiplier
let centerEffects = controls.centerEffects
let centerRadius = controls.centerRadius //% of line
let centerHue = controls.centerColor
let radius = ((factor / Coord.points.filter((point) => point.row == 1).length) * pointRadius)
const normalGrowth = growthFactor - .5
radius = radius + (normalGrowth * radius * 2) //max gf = 1, min = .5.
let inner, outer
const dotHue = controls.pointColor * (growthFactor)
var tiny = tinycolor(`hsl(${dotHue}, 100%, 50%)`)
var color = tiny.toHexString()
var complement = tinycolor(color).spin(-180).toHexString()
var triad = tinycolor(color).spin(-120).toHexString()
var tetrad = tinycolor(color).spin(-90).toHexString()
var analog = tinycolor(color).spin(-30).toHexString()
var twin = tinycolor(color).spin(10).toHexString()
switch (controls.gridPointsColorFunc) {
case "complement":
outer = growthFactor > .9 ? complement : growthFactor > .8 ? triad : growthFactor > .7 ? tetrad : growthFactor > .6 ? analog : color
inner = color
break
case "triad":
inner = color
outer = growthFactor > .85 ? triad : growthFactor > .75 ? tetrad : color
break
case "tetrad":
outer = color
inner = growthFactor > .85 ? tetrad : growthFactor > .75 ? analog : color
break
case "analog":
outer = growthFactor > .75 ? analog : twin
inner = color
break
case "twin":
inner = twin
outer = color
break
}
let centerColor
let flat = tinycolor(`hsl(${centerHue}, 100%, 50%)`)
let tinygrow = tinycolor(`hsl(${centerHue*growthFactor}, 100%, 50%)`)
switch (controls.centerColorFunc) {
case "burst3":
centerColor = growthFactor > .9 ? tinygrow.spin(180).toHexString() : growthFactor > .8 ? tinygrow.spin(120).toHexString() : growthFactor > .7 ? tinygrow.spin(90).toHexString() : growthFactor > .6 ? tinygrow.spin(30).toHexString() : tinygrow.toHexString()
break
case "burst2":
centerColor = growthFactor > .85 ? tinygrow.spin(120).toHexString() : growthFactor > .75 ? tinygrow.spin(90).toHexString() : tinygrow.toHexString()
break
case "burst1":
centerColor = growthFactor > .85 ? tinygrow.spin(90).toHexString() : growthFactor > .75 ? tinygrow.spin(30).toHexString() : tinygrow.toHexString()
break
case "flat2":
centerColor = growthFactor > .85 ? flat.spin(90).toHexString() : growthFactor > .75 ? flat.spin(30).toHexString() : flat.toHexString()
break
case "flat1":
centerColor = growthFactor > .85 ? flat.spin(30).toHexString() : flat.toHexString()
break
}
let origin = Coord.origin()
if (centerEffects != "none") {
drawCircle({
data: data,
baseRadius: centerRadius,
multiplier: centerMultiplier,
centerEffects: centerEffects,
point: origin,
color: centerColor
})
}
Coord.points.forEach((point, i) => {
if (point.endpoint || (!point.hide && controls.gridPoints)) {
if (!point.endpoint) {
if (growthFactor > peak) {
if (point !== origin || controls.centerEffects === "none") Circle(point, radius, outer, inner, pointStyle)
}
} else {
if (controls.endPoints) {
Circle(point, radius, outer, inner, pointStyle)
}
}
}
})
function drawCircle(options) {
let totalLength = options.data.length
let mult = options.multiplier
let originX = options.point.x
let originY = options.point.y
let data = options.data
let lw = ctx.lineWidth
let r = options.baseRadius
let centerEffects = options.centerEffects
//ctx.save()
ctx.beginPath()
data.forEach((w, i) => {
let degrees = 270 + (i / totalLength) * 360
let rads = degrees * (Math.PI / 180)
let cosT = Math.cos(rads)
let sinT = Math.sin(rads)
let cx = r * cosT + originX
let cy = r * sinT + originY
let val = circleValue(w, i, canvas.height, mult) + r
let cx2 = val * cosT + originX
let cy2 = val * sinT + originY
switch (centerEffects) {
case "bars": {
ctx.strokeStyle = options.color
ctx.moveTo(cx, cy)
ctx.lineTo(cx2, cy2)
break;
}
case "dots": {
ctx.fillStyle = options.color
ctx.fillRect(cx2, cy2, lw, lw)
break;
}
}
})
ctx.stroke()
//ctx.restore()
function circleValue(byteValue, index, availableWidth, multiplier) {
return (byteValue / MAXBYTE * (availableWidth)) * multiplier
}
}
}
var Circle = (p, r, color1, color2, style) => {
ctx.beginPath();
ctx.arc(p.x, p.y, r, 0, PI2, true)
ctx.strokeStyle = color1
ctx.fillStyle = color1
switch (style) {
case 'stroke':
ctx.stroke();
break;
case 'fill':
ctx.fill();
break;
}
ctx.beginPath()
ctx.arc(p.x, p.y, (3 * r) / 4, 0, PI2, true)
ctx.strokeStyle = color2
ctx.fillStyle = color2
switch (style) {
case 'stroke':
ctx.stroke();
break;
case 'fill':
ctx.fill();
break;
}
}
/********************* HELPERS **********************/
function setBackgroundColorsFromConfig() {
let body = document.getElementsByTagName("body")[0]
body.style.backgroundColor = controls.backgroundColor
controls.color = readableComplement(controls.backgroundColor);
body.style.color = controls.color;
[...document.querySelectorAll("input, select, a")].forEach(input => {
input.style.backgroundColor = controls.backgroundColor
input.style.color = controls.color
})
}
function setSizingInfo() {
const cssWidth = clientWidth = container.clientWidth;
const cssHeight = clientHeight = container.clientHeight;
factor = clientWidth > clientHeight ? clientHeight : clientWidth
offsetY = clientHeight * .04
offsetX = clientWidth * .04
cy_FPS = offsetY / 2
cx_FPS = clientWidth - (offsetX)
if (canvas.width !== cssWidth || canvas.height !== cssHeight) {
canvas.width = cssWidth;
canvas.height = cssHeight;
}
ORIGIN = new Point(canvas.width / 2, canvas.height / 2)
//console.log(`factor:${factor},linewidth:${ctx.lineWidth}`)
}
var readAudioFile = (context, file) => {
var reader = new FileReader();
var src = context.createBufferSource();
return new Promise((resolve, reject) => {
reader.onload = function (e) {
var arrayBuffer = e.currentTarget.result;
context.decodeAudioData(arrayBuffer, function (audioBuffer) {
src.buffer = audioBuffer;
resolve(src);
})
}
reader.readAsArrayBuffer(file);
})
}
const songChanged = () => {
file = document.querySelector("#songDropdown").files[0];
if (!file) return false;
if (audioContext) audioContext.close();
audioContext = new AudioContext()
//change to spinner while loading:
pauseButton = document.querySelector(".pause")
pauseButton.style["pointer-events"] = "none"
pauseButton.classList.remove("fa-play")
pauseButton.classList.add("fa-pause")
pauseButton.classList.add("fa-spin")
readAudioFile(audioContext, file).then(srcNode => {
srcNode.onended = function (event) {
audioContext.close();
playing = false;
//if we get to end of track change button back to play.
pauseButton.classList.remove("fa-pause")
pauseButton.classList.add("fa-play")
}.bind(this);
analyzeTrack(audioContext, srcNode);
//song should be playing, so stop spinning:
pauseButton.style["pointer-events"] = "auto"
pauseButton.classList.remove("fa-spin")
})
}
const autoPlay = (url) => {
if (audioContext) audioContext.close();
audioContext = new AudioContext()
let audio = new Audio()
audio.crossOrigin = "anonymous"
audio.src = url
audio.type = "audio/mpeg"
audio.preload = "preload"
audio.autoplay = "autoplay"
//audio.volume = 1;
let src = audioContext.createMediaElementSource(audio);
analyzeTrack(audioContext, src)
}
window.pauseSong = (el) => {
if (!audioContext) {
return false;
}
pauseButton = document.querySelector(".pause");
if (audioContext.state === 'running') {
//pause:
audioContext.suspend().then(function () {
playing = false;
pauseButton.classList.remove("fa-pause")
pauseButton.classList.add("fa-play")
}.bind(this));
} else if (!playing && audioContext.state === 'suspended') {
//resume:
audioContext.resume().then(function () {
playing = true;
pauseButton.classList.remove("fa-play")
pauseButton.classList.add("fa-pause")
}.bind(this));
} else if (!playing && audioContext.state === 'closed') {
//play last selected song:
songChanged();
} else {
}
} //TODO export these public functions!
function CartesianCoord(options) {
var self = this
var width = options.width
var height = options.height
this.rows = options.gridSize
this.cols = options.gridSize
//offsetX, offsetY
this.origin = () => {
return this.points[Math.floor(this.points.length / 2)]
}
/*top*/
this.topLeft = () => {
return this.points[0]
}
this.topRight = () => {
return this.points.filter(p => p.row == 1).slice(-1)[0]
}
this.topMid = () => {
let firstRow = this.points.filter(p => p.row == 1)
return firstRow[Math.floor(firstRow.length / 2)]
}
this.top25 = () => {
let firstRow = this.points.filter(p => p.row == 1)
return firstRow[Math.floor(firstRow.length / 4)]
}
this.top75 = () => {
let firstRow = this.points.filter(p => p.row == 1)
return firstRow[Math.floor((firstRow.length / 4) * 3)]
}
/*bottom*/
this.bottomLeft = () => {
return this.points.filter(p => p.col == 1).slice(-1)[0]
}
this.bottomMid = () => {
let lastRow = this.points.filter(p => p.row == this.rows)
return lastRow[Math.floor(lastRow.length / 2)]
}
this.bottomRight = () => {
return this.points[this.points.length - 1]
}
this.bottom25 = () => {
let lastRow = this.points.filter(p => p.row == this.rows)
return lastRow[Math.floor(lastRow.length / 4)]
}
this.bottom75 = () => {
let lastRow = this.points.filter(p => p.row == this.rows)
return lastRow[Math.floor((lastRow.length / 4) * 3)]
}
/*left */
this.leftMid = () => {
let firstCol = this.points.filter(p => p.col == 1)
return firstCol[Math.floor(firstCol.length / 2)]
}
this.left25 = () => {
let firstCol = this.points.filter(p => p.col == 1)
return firstCol[Math.floor(firstCol.length / 4)]
}
this.left75 = () => {
let firstCol = this.points.filter(p => p.col == 1)
return firstCol[Math.floor((firstCol.length / 4) * 3)]
}
/*right*/
this.right25 = () => {
let lastCol = this.points.filter(p => p.col == this.cols)
return lastCol[Math.floor(lastCol.length / 4)]
}
this.rightMid = () => {
let lastCol = this.points.filter(p => p.col == this.cols)
return lastCol[Math.floor(lastCol.length / 2)]
}
this.right75 = () => {
let lastCol = this.points.filter(p => p.col == this.cols)
return lastCol[Math.floor((lastCol.length / 4) * 3)]
}
/*center vertical */
this.centerY75 = () => {
let middleCol = this.points.filter(p => p.col == Math.ceil(this.cols / 2))
let delta = Math.floor((middleCol.length / 4) * 3)
return middleCol[delta]
}
this.centerY25 = () => {
let middleCol = this.points.filter(p => p.col == Math.ceil(this.cols / 2))
let delta = Math.floor(middleCol.length / 4)
return middleCol[delta]
}
/*center horizontal*/
this.centerX75 = () => {
let middleRow = this.points.filter(p => p.row == Math.ceil(this.rows / 2))
let delta = Math.floor((middleRow.length / 4) * 3)
return middleRow[delta]
}
this.centerX25 = () => {
let middleRow = this.points.filter(p => p.row == Math.ceil(this.rows / 2))
let delta = Math.floor(middleRow.length / 4)
return middleRow[delta]
}
/* quads */
this.quad1Center = () => {
let rowNum = Math.ceil(this.rows * .25)
return this.points.filter(p => p.row === rowNum && p.col === rowNum)[0]
}
this.quad2Center = () => {
let rowNum = Math.ceil(this.rows * .25)
let colNum = Math.ceil(this.rows * .75)
return this.points.filter(p => p.row === rowNum && p.col === colNum)[0]
}
this.quad3Center = () => {
let rowNum = Math.ceil(this.rows * .75)
let colNum = Math.ceil(this.rows * .25)
return this.points.filter(p => p.row === rowNum && p.col === colNum)[0]
}
this.quad4Center = () => {
let rowNum = Math.ceil(this.rows * .75)
return this.points.filter(p => p.row === rowNum && p.col === rowNum)[0]
}
this.pointFunctions = [
{
key: "origin",
name: "origin",
value: this.origin
},
//top
{
key: "topLeft",
name: "top left",
value: this.topLeft
},
{
name: "Top Right",
key: "topRight",
value: this.topRight
},
{
key: "topMid",
name: "top Mid",
value: this.topMid
},
{
name: "top 25%",
key: "top25",
value: this.top25
},
{
key: "top75",
name: "top 75%",
value: this.top75
},
//bottom
{
key: "bottomMid",
name: "bottom Mid",
value: this.bottomMid
},
{
key: "bottomLeft",
name: "bottom Left",
value: this.bottomLeft
},
{
name: "bottom Right",
key: "bottomRight",
value: this.bottomRight
},
{
key: "bottom25",
name: "bottom 25%",
value: this.bottom25
},
{
name: "bottom 75%",
key: "bottom75",
value: this.bottom75
},
//left side
{
name: "left Mid",
key: "leftMid",
value: this.leftMid
},
{
name: "left 25%",
key: "left25",
value: this.left25
},
{
key: "left75",
name: "left 75%",
value: this.left75
},
//right side
{
key: "rightMid",
name: "right Mid",
value: this.rightMid
},
{
key: "right25",
name: "right 25%",
value: this.right25
},
{
key: "right75",
name: "right 75%",
value: this.right75
},
//center X Axis
{
key: "centerX25",
name: "center X 25%",
value: this.centerX25
},
{
key: "centerX75",
name: "center X 75%",
value: this.centerX75
},
//center Y AYis
{
key: "centerY25",
name: "center Y 25%",
value: this.centerY25
},
{
key: "centerY75",
name: "center Y 75%",
value: this.centerY75
},
//Quadrant centers
{
key: "quad1",
name: "Quad 1 center",
value: this.quad1Center
},
{
key: "quad2",
name: "Quad 2 center",
value: this.quad2Center
},
{
key: "quad3",
name: "Quad 3 center",
value: this.quad3Center
},
{
key: "quad4",
name: "Quad 4 center",
value: this.quad4Center
},
]
this.determineGridPoints = function () {
self.points = []
let totalWidth = width - options.offsetX * 2
let totalHeight = height - options.offsetY * 2
for (var i = 0; i < this.rows; i++) {
var deltaY = (i / (this.rows - 1)) * totalHeight
for (var j = 0; j < this.cols; j++) {
var deltaX = (j / (this.cols - 1)) * totalWidth
let x = deltaX + options.offsetX
let y = deltaY + options.offsetY
let p = new Point(x, y)
p.row = i + 1
p.col = j + 1
//console.log("pushing point:" + p.row + "," + p.col)
self.points.push(p)
}
}
}
self.determineGridPoints()
self.markPoints = (shapeKey) => {
let shape = self.getShape(shapeKey)
var m, b
shape.lines.forEach((line, i) => {
let p1 = line.p1
p1.endpoint = true
let p2 = line.p2
p2.endpoint = true
const deltaY = p2.row - p1.row
const deltaX = p2.col - p1.col
m = deltaY / deltaX
b = p1.row - m * p1.col
if (m === Infinity || m === -Infinity) {
iterateAndMark(p1, p2, "row", "col")
} else {
iterateAndMark(p1, p2, "col", "row")
}
})
function iterateAndMark(p1, p2, ind, dep) {
const parity = p2[ind] < p1[ind] ? -1 : 1
const range = Math.abs(p2[ind] - p1[ind]) - 1
for (var i = 1; i <= range; i++) {
const varied = p1[ind] + (parity * i)
const target = ind === "row" ? p1[dep] : m * varied + b
let point = self.points.find(p => p[ind] == varied && p[dep] == target)
if (point) {
//console.log(`point hide (x): ${point.row},${point.col}`)
point.hide = true
}
}
}
}
self.getShape = function (key, options) {
let shape = self.shapes.find(function (s) {
return s.key === key
})
return shape
}
self.getNamesFromConfig = (lineConfigs) => {
let names = []
lineConfigs.forEach(config => {
names.push(config.line[0])
})
if (lineConfigs.length === 1) {
names.push(lineConfigs[0].line[1])
}
return names
}
self.generateLinesFromConfig = (lineConfigs) => {
let lines = []
lineConfigs.forEach(config => {
let p1 = self.pointFunctions.find(pf => pf.key === config.line[0]).value()
let p2 = self.pointFunctions.find(pf => pf.key === config.line[1]).value()
lines.push(new Line(p1, p2, {
inverted: config.inverted
}))
})
return lines
}
//SHAPE INIT
self.shapes = new DefaultShapes()
//generate shapes for custom shapes in themes
options.themes.forEach(theme => {
theme.styledShapes.forEach(ss => {
if (ss.shapeKey === undefined || /^custom/i.test(ss.shapeKey)) {
let key = ss.shapeKey
if(key === undefined) key = `Custom_${ss.lineConfigs.length}_${GUID()}_${theme.key}`
let lines = self.generateLinesFromConfig(ss.lineConfigs)
let customShapeForTheme = {
name: key,
key: key,
type: "custom",
lines: lines,
lineConfigs: ss.lineConfigs
}
ss.shapeKey = key
self.shapes.push(customShapeForTheme)
}
})
})
//SHAPE UPDATE
self.updateShapeLinesFromConfig = () => {
self.shapes.forEach(sh => {
sh.lines = self.generateLinesFromConfig(sh.lineConfigs)
})
}
self.updateShapeLinesFromConfig()
/*helpers*/
function DefaultShapes() {
return [
{
name: "Outer Box",
key: "OuterBox",
type: "preset",
lineConfigs: [{
line: ["topRight", "topLeft"]
},
{
line: ["topRight", "bottomRight"]
},
{
line: ["bottomLeft", "bottomRight"],
inverted: true
},
{
line: ["bottomLeft", "topLeft"]
}
]
},
{
name: "X",
key: "X",
type: "preset",
lineConfigs: [{
line: ["topLeft", "bottomRight"]
},
{
line: ["topRight", "bottomLeft"]
}
]
},
{
name: "X (anchored)",
key: "XAnchored",
type: "preset",
lineConfigs: [{
line: ["topLeft", "origin"]
},
{
line: ["topRight", "origin"],
inverted: true
},
{
line: ["bottomLeft", "origin"]
},
{
line: ["bottomRight", "origin"]
}
]
},
{
name: "Diamond (suit) ♦",
key: "Diamond",
type: "preset",
lineConfigs: [{
line: ["topMid", "rightMid"]
},
{
line: ["bottomMid", "rightMid"]
},
{
line: ["bottomMid", "leftMid"]
},
{
line: ["leftMid", "topMid"]
}
]
},
{
name: "Diamond (jewel)",
key: "Jewel",
type: "preset",
lineConfigs: [{
line: ["left25", "top25"]
},
{
line: ["bottomMid", "left25"]
},
{
line: ["bottomMid", "right25"]
},
{
line: ["right25", "top75"]
},
{
line: ["top75", "top25"]
}
]
},
{
name: "Greek Cross ✚",
key: "GreekCross",
type: "preset",
lineConfigs: [{
line: ["leftMid", "rightMid"]
},
{
line: ["bottomMid", "topMid"]
}
]
},
{
name: "Greek Cross ✚ (anchored)",
key: "GreekCrossAnchored",
type: "preset",
lineConfigs: [{
line: ["topMid", "origin"]
},
{
line: ["rightMid", "origin"],
inverted:true
},
{
line: ["leftMid", "origin"]
},
{
line: ["bottomMid", "origin"]
},
]
},
{
name: "Latin Cross ✝",
key: "LatinCross",
type: "preset",
lineConfigs: [{
line: ["quad1", "quad2"]
},
{
line: ["topMid", "bottomMid"]
}
]
},
{
name: "Peace ☮",
key: "PeaceSign",
type: "preset",
lineConfigs: [{
line: ["topMid", "origin"]
},
{
line: ["origin", "quad3"]
},
{
line: ["origin", "quad4"]
},
{
line: ["origin", "centerY75"]
}
]
},
{
name: "Star of David ✡",
key: "StarOfDavid",
type: "preset",
lineConfigs: [{
line: ["topMid", "quad3"]
}, {
line: ["topMid", "quad4"]
}, {
line: ["quad3", "quad4"]
},
{
line: ["bottomMid", "quad1"]
}, {
line: ["bottomMid", "quad2"]
}, {
line: ["quad2", "quad1"]
}
]
},
{
name: "Infinity ∞",
key: "Infinity",
type: "preset",
lineConfigs: [{
line: ["quad1", "bottomRight"]
}, {
line: ["bottomRight", "topRight"]
}, {
line: ["topRight", "bottom25"]
}, {
line: ["bottom25", "quad1"]
}]
},
{
name: "Heart ❤",
key: "Heart",
type: "preset",
lineConfigs: [{
line: ["bottomMid", "left25"]
},
{
line: ["left25", "top25"]
},
{
line: ["top25", "centerY25"]
},
{
line: ["bottomMid", "right25"]
},
{
line: ["right25", "top75"]
},
{
line: ["top75", "centerY25"]
}
]
},
{
name: "Tree 🌲",
key: "Tree",
type: "preset",
lineConfigs: [{
line: ["bottomMid", "centerY75"]
}, {
line: ["centerY75", "quad3"]
}, {
line: ["quad3", "topMid"]
}, {
line: ["topMid", "quad4"]
}, {
line: ["quad4", "centerY75"]
}]
},
{
name: ":)",
key: "smiley",
type: "preset",
lineConfigs: [{
line: ["leftMid", "quad3"]
},
{
line: ["quad3", "quad4"]
},
{
line: ["quad4", "rightMid"]
},
{
line: ["quad1", "quad1"]
},
{
line: ["quad2", "quad2"]
},
]
}
]
} //end Shapes
function Line(p1, p2, options) {
this.p1 = p1
this.p2 = p2
this.inverted = (options && options.inverted) || false
}
} //end CartesianCoord
})("Visit me at sweaverD.com!");
Also see: Tab Triggers