/dfma/maps/2014-06/ajr_-Mountain of Immortal-region1-222-7581.fdf-map
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tests/unit/renderer-interface.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import unitUnderTest from '../../src/components/renderer.js'
3 |
4 | describe('Renderer Interface', () => {
5 | const viewState = {
6 | external: 'data'
7 | }
8 | const browserWindow = {}
9 |
10 | it('should expose the expected properties on the default interface', () => {
11 | const actual = Object.keys(unitUnderTest({ viewState }, browserWindow))
12 | expect(actual).to.deep.equal([
13 | 'draw',
14 | 'preload',
15 | 'setup'
16 | ])
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/.github/workflows/pr-check.yml:
--------------------------------------------------------------------------------
1 | name: Lint and test PR
2 | on:
3 | pull_request:
4 | branches:
5 | - master
6 | jobs:
7 | test:
8 | name: Linting and Testing
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v2
14 |
15 | - name: Use Node JS LTS
16 | uses: actions/setup-node@v2
17 | with:
18 | node-version: 16.x
19 |
20 | - name: Install npm dependencies
21 | run: npm install
22 |
23 | - name: Build project
24 | run: npm run build -s
25 |
26 | - name: Run default tests
27 | run: npm run test -s
28 |
--------------------------------------------------------------------------------
/tests/helpers/start-server.js:
--------------------------------------------------------------------------------
1 | import startServer from './integration-server.js'
2 |
3 | let closeTimeout
4 |
5 | before(async () => {
6 | if (closeTimeout) {
7 | console.log('[Integration Server] Keeping server alive')
8 | clearTimeout(closeTimeout)
9 | } else {
10 | console.log('[Integration Server] Starting server')
11 | }
12 | await startServer()
13 | })
14 |
15 | after(async () => {
16 | const { server } = await startServer()
17 | if (closeTimeout) {
18 | clearTimeout(closeTimeout)
19 | }
20 | closeTimeout = setTimeout(() => {
21 | clearTimeout(closeTimeout)
22 | server.close()
23 | console.log('[Integration Server] Server closed')
24 | }, 500)
25 | })
26 |
--------------------------------------------------------------------------------
/src/adapters/p5adapter.js:
--------------------------------------------------------------------------------
1 | function adaptToObject (fnName, object) {
2 | return (...args) => object[fnName](...args)
3 | }
4 |
5 | const methodsToAdapt = [
6 | 'alert',
7 | 'background',
8 | 'createCanvas',
9 | 'createImage',
10 | 'fill',
11 | 'floor',
12 | 'image',
13 | 'keyIsDown',
14 | 'max',
15 | 'noFill',
16 | 'line',
17 | 'loadPixels',
18 | 'pixelDensity',
19 | 'rect',
20 | 'stroke',
21 | 'strokeWeight',
22 | 'text',
23 | 'textAlign',
24 | 'textFont',
25 | 'updatePixels'
26 | ]
27 |
28 | export default (window) => {
29 | const methods = methodsToAdapt.reduce((acc, methodName) => {
30 | acc[methodName] = adaptToObject(methodName, window)
31 | return acc
32 | }, {})
33 | return methods
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/setup-starting-map.js:
--------------------------------------------------------------------------------
1 | import readMapInfoFromDocument from './read-map-info.js'
2 | import viewState from './viewState/model.js'
3 |
4 | async function setupStartingMap (document, { setMapByURL, zoomTo }) {
5 | const mapInfo = readMapInfoFromDocument(document)
6 | const map = await setMapByURL(mapInfo.mapLink)
7 | console.log('Starting map:', map, mapInfo)
8 |
9 | viewState.messages.push(`Starting map: start level: ${mapInfo.startLevel}, start zoom: ${mapInfo.startZoom}`)
10 | Object.assign(viewState, mapInfo)
11 |
12 | zoomTo(
13 | mapInfo.startLevel,
14 | mapInfo.startZoom,
15 | mapInfo.startX / map.tileWidth,
16 | mapInfo.startY / map.tileHeight
17 | )
18 | }
19 |
20 | export default setupStartingMap
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 DF Map Archive
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4 |
5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--------------------------------------------------------------------------------
/scripts/setBuildInformation.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path, { dirname } from 'path'
3 |
4 | import { fileURLToPath } from 'url'
5 |
6 | const __dirname = dirname(fileURLToPath(import.meta.url))
7 |
8 | let buildHash
9 | try {
10 | buildHash = fs.readFileSync(path.join(__dirname, '../build/buildhash.txt'), 'utf8')
11 | } catch (ex) {
12 | buildHash = 'UNKNOWN'
13 | }
14 |
15 | const date = new Date()
16 | const source = `https://github.com/df-map-archive/dfma-html5-map-viewer/commit/${buildHash}`
17 |
18 | const buildInfo = {
19 | buildHash,
20 | date,
21 | source
22 | }
23 |
24 | fs.writeFileSync(path.join(__dirname, '../src/buildInfo.json'), JSON.stringify(buildInfo, null, 2), 'utf8')
25 |
26 | console.log('[Set Build Information]', buildInfo)
27 |
--------------------------------------------------------------------------------
/tests/unit/user-inputs.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { join, dirname } from 'path'
3 | import jsdom from 'jsdom'
4 | import unitUnderTest from '../../src/components/user-inputs.js'
5 |
6 | import { fileURLToPath } from 'url'
7 | const __dirname = dirname(fileURLToPath(import.meta.url))
8 |
9 | const { JSDOM } = jsdom
10 |
11 | describe('User Inputs', () => {
12 | let document, body
13 | before(async () => {
14 | const { window } = await JSDOM.fromFile(join(__dirname, 'stubs/userInputs.html'))
15 | document = window.document
16 | body = document.querySelector('body')
17 | console.log('[User Inputs Test] Body ready for testing', body)
18 | })
19 |
20 | it('should export a function to test', () => {
21 | expect(typeof unitUnderTest).to.equal('function')
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/tests/unit/drag-and-drop.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { join, dirname } from 'path'
3 | import jsdom from 'jsdom'
4 | import unitUnderTest from '../../src/components/drag-and-drop.js'
5 |
6 | import { fileURLToPath } from 'url'
7 | const __dirname = dirname(fileURLToPath(import.meta.url))
8 |
9 | const { JSDOM } = jsdom
10 |
11 | describe('Drag and Drop', () => {
12 | let document, body
13 | before(async () => {
14 | const { window } = await JSDOM.fromFile(join(__dirname, 'stubs/dragAndDrop.html'))
15 | document = window.document
16 | body = document.querySelector('body')
17 | console.log('[Drag and Drop Test] Body ready for testing', body)
18 | })
19 |
20 | it('should export a function to test', () => {
21 | expect(typeof unitUnderTest).to.equal('function')
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/src/components/viewState/set-map-by-url.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Configure methods by injecting in shared viewState dependency
3 | *
4 | * @param {object} viewState the shared view model
5 | * @param {function} loadMapFromURL the fetcher for map data
6 | *
7 | * @return {MapData} the decoded map
8 | */
9 | export default (viewState, loadMapFromURL) => {
10 | /**
11 | * Set map to view by URL
12 | *
13 | * @param {string} mapUrl the URL of the fdf-map file to load and view
14 | */
15 | return async function (mapUrl) {
16 | if (document && document.getElementById) {
17 | const fileName = document.getElementById('fileName')
18 | if (fileName) {
19 | fileName.innerText = mapUrl
20 | }
21 | }
22 |
23 | // fetch local file
24 | const map = await loadMapFromURL(mapUrl)
25 | viewState.dfMapData = map
26 | return map
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Test and Deploy via FTP
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | deploy:
8 | name: Test and Deploy
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v2
14 |
15 | - name: Use Node JS LTS
16 | uses: actions/setup-node@v1
17 | with:
18 | node-version: 16.x
19 |
20 | - name: Install npm dependencies
21 | run: npm install
22 |
23 | - name: Build project
24 | run: npm run build -s
25 |
26 | - name: Run default tests
27 | run: npm run test -s
28 |
29 | - name: Upload files to website
30 | uses: sebastianpopp/ftp-action@releases/v2
31 | with:
32 | host: "ftp.mkv25.net"
33 | user: ${{ secrets.FTP_SYNC_USERNAME }}
34 | password: ${{ secrets.FTP_SYNC_PASSWORD }}
35 | localDir: "build"
--------------------------------------------------------------------------------
/tests/unit/stubs/mapInfo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Read Map Info Test Stub
5 |
6 |
7 |
8 | /dfma/maps/2020-03/mounf-Bellsshower-A_library-251-110.fdf-map
9 | A comunity/story fort aiming to build a library and fort across all 3 cavern levels. I've not explored the library mechanics before.
10 | 68
11 | 1824
12 | 976
13 | 1.00
14 | top
15 | Temporary fort
16 | The lower level of the dining hall - this is temporary whilst preparing to breach and secure the upper cavern.
17 | mounf
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.github/workflows/publish-tag.yml:
--------------------------------------------------------------------------------
1 | name: Publish tag via FTP
2 | on:
3 | create:
4 | tags:
5 | - 'v*'
6 | - 'latest'
7 |
8 | jobs:
9 | test:
10 | name: Build and release based on tag
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v2
16 |
17 | - name: Use Node JS LTS
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: 16.x
21 |
22 | - name: Install npm dependencies
23 | run: npm install
24 |
25 | - name: Build project
26 | run: npm run build -s
27 | env:
28 | GITHUB_REF: $GITHUB_REF
29 |
30 | - name: Run default tests
31 | run: npm run test -s
32 |
33 | - name: Upload files to website
34 | uses: sebastianpopp/ftp-action@releases/v2
35 | with:
36 | host: "ftp.mkv25.net"
37 | user: ${{ secrets.FTP_SYNC_USERNAME }}
38 | password: ${{ secrets.FTP_SYNC_PASSWORD }}
39 | localDir: "build"
--------------------------------------------------------------------------------
/licenses/DFMA Viewer HTML5 LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Jacob Blomquist
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/licenses/PAKO LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/src/fullscreen.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DFMA Map Viewer - Fullscreen Render Test
7 |
8 |
9 |
10 |
11 |
12 | /dfma/maps/2020-03/mounf-Bellsshower-A_library-251-110.fdf-map
13 | A comunity/story fort aiming to build a library and fort across all 3 cavern levels. I've not explored the library mechanics before.
14 | 68
15 | 1824
16 | 976
17 | 1.00
18 | top
19 | Temporary fort
20 | The lower level of the dining hall - this is temporary whilst preparing to breach and secure the upper cavern.
21 | mounf
22 | true
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/licenses/PAKO (ZLIB) LICENSE:
--------------------------------------------------------------------------------
1 | Acknowledgments:
2 |
3 | The deflate format used by zlib was defined by Phil Katz. The deflate and
4 | zlib specifications were written by L. Peter Deutsch. Thanks to all the
5 | people who reported problems and suggested various improvements in zlib; they
6 | are too numerous to cite here.
7 |
8 | Copyright notice:
9 |
10 | (C) 1995-2013 Jean-loup Gailly and Mark Adler
11 |
12 | Copyright (c) <''year''> <''copyright holders''>
13 |
14 | This software is provided 'as-is', without any express or implied
15 | warranty. In no event will the authors be held liable for any damages
16 | arising from the use of this software.
17 |
18 | Permission is granted to anyone to use this software for any purpose,
19 | including commercial applications, and to alter it and redistribute it
20 | freely, subject to the following restrictions:
21 |
22 | 1. The origin of this software must not be misrepresented; you must not
23 | claim that you wrote the original software. If you use this software
24 | in a product, an acknowledgment in the product documentation would be
25 | appreciated but is not required.
26 | 2. Altered source versions must be plainly marked as such, and must not be
27 | misrepresented as being the original software.
28 | 3. This notice may not be removed or altered from any source distribution.
29 |
30 |
31 | Jean-loup Gailly Mark Adler
32 | jloup@gzip.org madler@alumni.caltech.edu
--------------------------------------------------------------------------------
/tests/unit/rewrite-map-links.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { join, dirname } from 'path'
3 | import jsdom from 'jsdom'
4 | import unitUnderTest from '../../src/components/rewrite-map-links.js'
5 |
6 | import { fileURLToPath } from 'url'
7 | const __dirname = dirname(fileURLToPath(import.meta.url))
8 |
9 | const { JSDOM } = jsdom
10 |
11 | describe('Rewrite map links', () => {
12 | let document
13 | before(async () => {
14 | const { window } = await JSDOM.fromFile(join(__dirname, 'stubs/mapLinks.html'), { pretendToBeVisual: true })
15 | document = window.document
16 | })
17 |
18 | it('should export a function to test', () => {
19 | expect(typeof unitUnderTest).to.equal('function')
20 | })
21 |
22 | it('should attach functions to the click event of map-links ', () => {
23 | const clicks = []
24 | function callback (href) {
25 | clicks.push(href)
26 | }
27 | unitUnderTest(document.querySelector('body'), { setMapByURL: callback })
28 | const mapLinks = Array.from(document.getElementsByTagName('map-link'))
29 | mapLinks.forEach(el => {
30 | el.click()
31 | })
32 |
33 | expect(clicks).to.deep.equal([
34 | '/dfma/maps/2008-02/alexencandar-Fortress of Rage-region1-1053-16119.fdf-map',
35 | '/dfma/maps/2010-03/memory-Gulfobeyed-region10-63-26010.fdf-map',
36 | '/dfma/maps/2014-06/ajr_-Mountain of Immortal-region1-222-7581.fdf-map'
37 | ])
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/tests/unit/read-map-info.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { join, dirname } from 'path'
3 | import jsdom from 'jsdom'
4 | import unitUnderTest from '../../src/components/read-map-info.js'
5 |
6 | import { fileURLToPath } from 'url'
7 | const __dirname = dirname(fileURLToPath(import.meta.url))
8 |
9 | const { JSDOM } = jsdom
10 |
11 | describe('Read map info', () => {
12 | let document, body
13 | before(async () => {
14 | const { window } = await JSDOM.fromFile(join(__dirname, 'stubs/mapInfo.html'))
15 | document = window.document
16 | body = document.querySelector('body')
17 | })
18 |
19 | it('should export a function to test', () => {
20 | expect(typeof unitUnderTest).to.equal('function')
21 | })
22 |
23 | it('should return an object with values parsed from the document', () => {
24 | const actual = unitUnderTest(body)
25 | expect(actual).to.deep.equal({
26 | mapDescription: 'A comunity/story fort aiming to build a library and fort across all 3 cavern levels. I\'ve not explored the library mechanics before.',
27 | mapLink: '/dfma/maps/2020-03/mounf-Bellsshower-A_library-251-110.fdf-map',
28 | startLevel: 68,
29 | startX: 1824,
30 | startY: 976,
31 | startZoom: 1.0,
32 | startOrientation: 'top',
33 | poiTitle: 'Temporary fort',
34 | poiDescription: 'The lower level of the dining hall - this is temporary whilst preparing to breach and secure the upper cavern.',
35 | poiAuthor: 'mounf',
36 | hideText: false
37 | })
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/src/components/viewState/zoom.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Configure methods by injecting in shared viewState dependency
3 | *
4 | * @param {object} viewState
5 | */
6 | export default (viewState) => {
7 | const jump = 1.05
8 |
9 | /**
10 | * Zoom in by a set amount
11 | */
12 | function zoomIn () {
13 | viewState.scale *= jump
14 | if (viewState.scale > 20) {
15 | viewState.scale = 20
16 | }
17 | zoom()
18 | }
19 |
20 | /**
21 | * Zoom out by a set amount
22 | */
23 | function zoomOut () {
24 | viewState.scale /= jump
25 | if (viewState.scale < 0.01) {
26 | viewState.scale = 0.01
27 | }
28 | zoom()
29 | }
30 |
31 | /**
32 | * Update the view position based on a change in scale
33 | */
34 | function zoom () {
35 | const { canvasWidth, canvasHeight } = viewState
36 |
37 | const curCenterX = canvasWidth / 2 - viewState.imageX
38 | const curCenterY = canvasHeight / 2 - viewState.imageY
39 |
40 | const ratioX = curCenterX / viewState.imgWidth
41 | const ratioY = curCenterY / viewState.imgHeight
42 |
43 | viewState.imgWidth = viewState.originalImgWidth * viewState.scale
44 | viewState.imgHeight = viewState.originalImgHeight * viewState.scale
45 |
46 | viewState.imageX = canvasWidth / 2 - viewState.imgWidth * ratioX
47 | viewState.imageY = canvasHeight / 2 - viewState.imgHeight * ratioY
48 | viewState.originalImgWidth = 0
49 |
50 | viewState.messages.push(`Zoomed at: ${viewState.scale.toFixed(3)}`)
51 | }
52 |
53 | return {
54 | zoomIn,
55 | zoomOut
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/map-viewer.js:
--------------------------------------------------------------------------------
1 | import parser from './components/parser'
2 | import renderer from './components/renderer'
3 | import viewState from './components/viewState/model'
4 | import rewriteMapLinks from './components/rewrite-map-links'
5 | import userInputs from './components/user-inputs'
6 | import zoom from './components/viewState/zoom'
7 | import zoomTo from './components/viewState/zoom-to'
8 |
9 | import setupStartingMap from './components/setup-starting-map'
10 | import dragAndDrop from './components/drag-and-drop'
11 | import setMapByURL from './components/viewState/set-map-by-url'
12 | import { loadMapFromURL, loadMapFromFileSystem } from './components/readers'
13 |
14 | const p5 = require('p5')
15 | const buildInfo = require('./buildInfo.json')
16 |
17 | function setup () {
18 | let mapRenderer
19 | if (typeof window !== 'undefined') {
20 | const { zoomIn, zoomOut } = zoom(viewState)
21 | mapRenderer = renderer({ viewState, zoomIn, zoomOut }, window)
22 |
23 | window.p5 = p5
24 | window.buildInfo = buildInfo
25 | Object.assign(window, parser)
26 | Object.assign(window, mapRenderer)
27 |
28 | userInputs(window, { viewState })
29 | }
30 |
31 | if (typeof document !== 'undefined') {
32 | rewriteMapLinks(document, { setMapByURL: mapRenderer.setMapByURL })
33 | setupStartingMap(document, { setMapByURL: setMapByURL(viewState, loadMapFromURL), zoomTo: zoomTo(viewState) })
34 | dragAndDrop(document, window, { viewState, loadMapFromFileSystem })
35 | }
36 | }
37 |
38 | try {
39 | setup()
40 | } catch (ex) {
41 | console.log(`map-viewer.js Encounted error: ${ex.message}`, ex)
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/drag-and-drop.js:
--------------------------------------------------------------------------------
1 | function registerOn (document, browserWindow, { viewState, loadMapFromFileSystem }) {
2 | const dropTarget = document.getElementById('p5-dfma-html5-map-viewer')
3 |
4 | console.log('[Drag and Drop]', 'Registered drop target', dropTarget)
5 |
6 | dropTarget.ondragover = fileHover
7 | dropTarget.ondragleave = fileHoverLeave
8 | dropTarget.ondrop = fileDrop
9 |
10 | function allowDrop (ev) {
11 | ev.preventDefault()
12 | }
13 |
14 | /**
15 | * Callback for when a hover event occurs
16 | */
17 | function fileHover (ev) {
18 | allowDrop(ev)
19 | viewState.dragged = true
20 | }
21 |
22 | /**
23 | * Call back for when a hover event leaves canvas
24 | */
25 | function fileHoverLeave () {
26 | viewState.dragged = false
27 | }
28 |
29 | /**
30 | * Callback for when a file drop event occurs
31 | */
32 | async function fileDrop (ev) {
33 | allowDrop(ev)
34 | const { dataTransfer } = ev
35 | const { files } = dataTransfer
36 | console.log(files)
37 | const dropFile = files[0]
38 |
39 | if (!dropFile.name.endsWith('fdf-map')) {
40 | browserWindow.alert("Invalid File Format! You must submit an 'fdf-map' file!")
41 | }
42 |
43 | // Reset draw on existing map
44 | viewState.originalImgWidth = 0
45 | viewState.originalImgHeight = 0
46 |
47 | fileHoverLeave()
48 | if (document.getElementById('fileName')) {
49 | document.getElementById('fileName').innerText = dropFile.name
50 | }
51 |
52 | viewState.dfMapData = await loadMapFromFileSystem(dropFile)
53 | }
54 | }
55 |
56 | export default registerOn
57 |
--------------------------------------------------------------------------------
/tests/unit/setup-starting-map.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { join, dirname } from 'path'
3 | import jsdom from 'jsdom'
4 | import unitUnderTest from '../../src/components/setup-starting-map.js'
5 |
6 | import { fileURLToPath } from 'url'
7 | const __dirname = dirname(fileURLToPath(import.meta.url))
8 |
9 | const { JSDOM } = jsdom
10 |
11 | const stubMap = { tileWidth: 16, tileHeight: 16 }
12 |
13 | describe('Setup starting map', () => {
14 | let document, body
15 | before(async () => {
16 | const { window } = await JSDOM.fromFile(join(__dirname, 'stubs/mapInfo.html'))
17 | document = window.document
18 | body = document.querySelector('body')
19 | })
20 |
21 | it('should export a function to test', () => {
22 | expect(typeof unitUnderTest).to.equal('function')
23 | })
24 |
25 | it('should read values from the document, and call setMapByURL', async () => {
26 | function callback (mapLink) {
27 | expect(mapLink).to.equal('/dfma/maps/2020-03/mounf-Bellsshower-A_library-251-110.fdf-map')
28 | return stubMap
29 | }
30 | return unitUnderTest(body, { setMapByURL: callback, zoomTo: () => null })
31 | })
32 |
33 | it('should read values from the document, and call zoomTo', async () => {
34 | function callback (startLevel, startZoom, startX, startY) {
35 | expect({
36 | startLevel,
37 | startZoom,
38 | startX,
39 | startY
40 | }).to.deep.equal({
41 | startLevel: 68,
42 | startZoom: 1,
43 | startX: 1824 / stubMap.tileWidth,
44 | startY: 976 / stubMap.tileHeight
45 | })
46 | }
47 | return unitUnderTest(body, { setMapByURL: () => stubMap, zoomTo: callback })
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/tests/helpers/integration-server.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import express from 'express'
3 | import { fileURLToPath } from 'url'
4 |
5 | const __dirname = path.dirname(fileURLToPath(import.meta.url))
6 | const servers = {}
7 |
8 | async function logAllRequests (app) {
9 | app.use(function (req, res, next) {
10 | console.log('[Integration Server] [Log All Requests]', req.originalUrl)
11 | next()
12 | })
13 | }
14 |
15 | async function setupStaticRoutes (app) {
16 | const serverPath = path.join(__dirname, '../../build/xdfmadev/parcel')
17 | console.log('[Integration Server] [Setup Static Routes]', serverPath)
18 | app.use('/', express.static(serverPath))
19 | }
20 |
21 | async function startServer (port = 9757) {
22 | if (servers[port]) {
23 | return Promise.resolve(servers[port])
24 | }
25 |
26 | let serverReady, serverError
27 | const ready = new Promise((resolve, reject) => {
28 | serverReady = resolve
29 | serverError = reject
30 | })
31 |
32 | servers[port] = ready
33 | const serverInfo = {}
34 |
35 | try {
36 | const app = express()
37 | const steps = [logAllRequests, setupStaticRoutes]
38 | await Promise.all(steps.map(async fn => {
39 | await fn(app)
40 | }))
41 |
42 | const server = app.listen(port, function () {
43 | const port = server.address().port
44 | console.log('[DFMA HTML5 Map Viewer] Integration Server listening at http://%s:%s/', 'localhost', port)
45 | serverInfo.basepath = `http://localhost:${port}/`
46 | serverReady(serverInfo)
47 | })
48 | serverInfo.server = server
49 | servers[port] = serverInfo
50 | } catch (ex) {
51 | serverError({
52 | basepath: serverInfo.basepath,
53 | server: serverInfo.server,
54 | error: ex
55 | })
56 | }
57 |
58 | return ready
59 | }
60 |
61 | export default startServer
62 |
--------------------------------------------------------------------------------
/src/components/read-map-info.js:
--------------------------------------------------------------------------------
1 | function readMapInfoFromDocument (parent) {
2 | const defaultMap = Array.from(parent.getElementsByTagName('default-map'))[0]
3 |
4 | function readTag (tagName) {
5 | return defaultMap.getElementsByTagName(tagName)[0].textContent
6 | }
7 |
8 | function readBooleanTag (tagName, defaultValue) {
9 | try {
10 | const tagValue = readTag(tagName) || defaultValue
11 | return !!JSON.parse(tagValue)
12 | } catch (ex) {
13 | return defaultValue
14 | }
15 | }
16 |
17 | function readIntegerTag (tagName, defaultValue) {
18 | try {
19 | const tagValue = readTag(tagName) || defaultValue
20 | return Number.parseInt(tagValue)
21 | } catch (ex) {
22 | return defaultValue
23 | }
24 | }
25 |
26 | function readFloatTag (tagName, defaultValue) {
27 | try {
28 | const tagValue = readTag(tagName) || defaultValue
29 | return Number.parseFloat(tagValue)
30 | } catch (ex) {
31 | return defaultValue
32 | }
33 | }
34 |
35 | function readStringTag (tagName, defaultValue) {
36 | try {
37 | return readTag(tagName) || defaultValue
38 | } catch (ex) {
39 | return defaultValue
40 | }
41 | }
42 |
43 | const mapInfo = {
44 | mapLink: readStringTag('map-link'),
45 | mapDescription: readStringTag('map-description'),
46 | startLevel: readIntegerTag('start-level', 0),
47 | startX: readIntegerTag('start-x', 0),
48 | startY: readIntegerTag('start-y', 0),
49 | startZoom: readFloatTag('start-zoom', 1.0),
50 | startOrientation: readStringTag('start-orientation'),
51 | poiTitle: readStringTag('poi-title'),
52 | poiDescription: readStringTag('poi-description'),
53 | poiAuthor: readStringTag('poi-author'),
54 | hideText: readBooleanTag('hide-text', false)
55 | }
56 |
57 | mapInfo.startZoom = mapInfo.startZoom || 1.0
58 |
59 | console.log('Map info (read-map-info.js):', mapInfo)
60 |
61 | return mapInfo
62 | }
63 |
64 | export default readMapInfoFromDocument
65 |
--------------------------------------------------------------------------------
/tests/integration/render-default-map.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { chromium } from 'playwright'
3 | import pixelmatch from 'pixelmatch'
4 | import path from 'path'
5 | import fs from 'fs'
6 | import { PNG } from 'pngjs'
7 | import '../helpers/start-server.js'
8 |
9 | import { fileURLToPath } from 'url'
10 | const __dirname = path.dirname(fileURLToPath(import.meta.url))
11 |
12 | function localPath (pathFragment) {
13 | return path.join(__dirname, pathFragment)
14 | }
15 |
16 | function readBinary (pathFragment) {
17 | const filepath = localPath(pathFragment)
18 | return fs.readFileSync(filepath)
19 | }
20 |
21 | describe('Render Default Map', () => {
22 | const width = 800
23 | const height = 600
24 | const padding = 50
25 |
26 | before(async function () {
27 | this.timeout(20000)
28 | // Make sure to run headed.
29 | const browser = await chromium.launch({ headless: true, width, height, padding })
30 |
31 | try {
32 | // Setup context however you like.
33 | const context = await browser.newContext({ /* pass any options */ })
34 | await context.route('**/*', route => route.continue())
35 |
36 | // Pause the page, and start recording manually.
37 | const page = await context.newPage()
38 | await page.goto('http://localhost:9757/fullscreen.html')
39 | await page.locator('canvas').screenshot({ path: localPath('results/default-map-actual.png'), width, height })
40 | } catch (ex) {
41 | console.error('Unable to setup page:', ex)
42 | }
43 |
44 | browser.close()
45 | })
46 |
47 | it('Should render map data to the page', () => {
48 | const actual = PNG.sync.read(readBinary('results/default-map-actual.png'))
49 | const expected = PNG.sync.read(readBinary('samples/default-map-expected.png'))
50 | const { width, height } = expected
51 | const diff = new PNG({ width, height })
52 |
53 | const result = pixelmatch(expected.data, actual.data, diff.data, width, height, { threshold: 0.1 })
54 | fs.writeFileSync(localPath('results/default-map-diff.png'), PNG.sync.write(diff))
55 |
56 | expect(result).to.deep.equal(0)
57 | })
58 | })
59 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dfma-html5-map-viewer",
3 | "version": "1.0.3",
4 | "description": "Home for development of a new HTML5 Map Viewer for Dwarf Fortress.",
5 | "main": "viewer.js",
6 | "type": "module",
7 | "scripts": {
8 | "setup": "./scripts/setup/installComposer.sh",
9 | "test": "standard && npm run test:unit",
10 | "test:unit": "mocha tests/unit/*.test.js",
11 | "test:integration": "mocha tests/integration/*.test.js",
12 | "lint": "standard --fix",
13 | "parcel": "parcel build src/map-viewer.js src/index.html src/fullscreen.html src/static/file.fdf-map --public-url ./",
14 | "build": "./build-script.sh",
15 | "start": "parcel src/index.html src/fullscreen.html src/static/file.fdf-map",
16 | "simulate-pr": "act pull_request -P ubuntu-latest=nektos/act-environments-ubuntu:18.04"
17 | },
18 | "standard": {
19 | "env": [
20 | "mocha"
21 | ]
22 | },
23 | "staticFiles": {
24 | "staticPath": "src/static",
25 | "watcherGlob": "*.fdf-map"
26 | },
27 | "repository": {
28 | "type": "git",
29 | "url": "git+https://github.com/df-map-archive/dfma-html5-map-viewer.git"
30 | },
31 | "keywords": [
32 | "DF",
33 | "Map",
34 | "Archive",
35 | "Viewer",
36 | "HTML",
37 | "5",
38 | "DFMA",
39 | "Dwarf",
40 | "Fortress",
41 | "JS",
42 | "Wrapper",
43 | "Jacob",
44 | "Blomquist",
45 | "P5"
46 | ],
47 | "author": "DF Map Archive",
48 | "contributors": [
49 | {
50 | "name": "Jacob Blomquist",
51 | "url": "https://github.com/JacobRBlomquist"
52 | },
53 | {
54 | "name": "John Beech",
55 | "url": "https://github.com/johnbeech"
56 | },
57 | {
58 | "name": "Max Heisinger",
59 | "url": "https://github.com/maximaximal"
60 | }
61 | ],
62 | "license": "ISC",
63 | "bugs": {
64 | "url": "https://github.com/df-map-archive/dfma-html5-map-viewer/issues"
65 | },
66 | "homepage": "https://github.com/df-map-archive/dfma-html5-map-viewer#readme",
67 | "dependencies": {
68 | "p5": "^1.4.1",
69 | "pako": "^2.0.4"
70 | },
71 | "devDependencies": {
72 | "@playwright/test": "^1.29.1",
73 | "babel-core": "^6.26.3",
74 | "babel-polyfill": "^6.26.0",
75 | "babel-preset-env": "^1.7.0",
76 | "chai": "^4.3.6",
77 | "esm": "^3.2.25",
78 | "express": "^4.18.1",
79 | "jsdom": "^19.0.0",
80 | "mocha": "^9.2.2",
81 | "parcel-bundler": "^1.12.5",
82 | "parcel-plugin-static-files-copy": "^2.6.0",
83 | "pixelmatch": "^5.3.0",
84 | "playwright": "^1.29.1",
85 | "pngjs": "^6.0.0",
86 | "standard": "^17.0.0"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DFMA Map Viewer - HTML5 Canvas
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
DFMA Map Viewer
15 |
16 |
Drag and Drop an FDF-MAP file to open.
17 |
18 |
19 |
20 | /dfma/maps/2020-03/mounf-Bellsshower-A_library-251-110.fdf-map
21 | A comunity/story fort aiming to build a library and fort across all 3 cavern levels. I've not explored the library mechanics before.
22 | 68
23 | 1824
24 | 976
25 | 1.00
26 | top
27 | Temporary fort
28 | The lower level of the dining hall - this is temporary whilst preparing to breach and secure the upper cavern.
29 | mounf
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
Extracted Tiles from:
38 |
39 |
40 |
/dfma/maps/2008-02/alexencandar-Fortress of Rage-region1-1053-16119.fdf-map
/dfma/maps/2014-06/ajr_-Mountain of Immortal-region1-222-7581.fdf-map
43 |
44 |
45 |
46 |
Controls:
47 |
48 |
Move Map
49 |
50 |
Click and Drag
51 |
52 |
53 |
Zoom
54 |
55 | Zoom out: -
56 |
57 |
Zoom in: + or =
58 |
59 |
60 |
Change Layer
61 |
Up: < or ,
62 |
Down: > or .
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/src/components/readers.js:
--------------------------------------------------------------------------------
1 | import { MapData } from './parser.js'
2 | import pako from 'pako'
3 |
4 | const browserWindow = (typeof window !== 'undefined') ? window : {}
5 | const fetch = browserWindow.fetch
6 |
7 | /**
8 | * Loads and parses a data file from a local user machine
9 | *
10 | * This only parses the data, it does NOT populate the image files.
11 | * These are generated and cached on-the-fly.
12 | *
13 | * @param {string} mapFile local filesystem path of the .fdf-map file to read
14 | *
15 | * @return {MapData} a mapData object that's ready to have its layers queried with mapData.getLayer()
16 | */
17 | export async function loadMapFromFileSystem (mapFile) {
18 | const reader = new browserWindow.FileReader()
19 | reader.readAsArrayBuffer(mapFile)
20 |
21 | let resolved, rejected
22 | const result = new Promise((resolve, reject) => {
23 | resolved = resolve
24 | rejected = reject
25 | })
26 |
27 | let mapData
28 | reader.onload = function () {
29 | try {
30 | const arr = new Uint8Array(reader.result)
31 | const data = pako.inflate(arr)
32 | const res = new DataView(data.buffer)
33 |
34 | mapData = new MapData()
35 | mapData.parse(res)
36 | resolved(mapData)
37 | } catch (ex) {
38 | rejected(ex)
39 | }
40 | }
41 |
42 | return result
43 | }
44 |
45 | /**
46 | * Uses Es6 Fetch to get a file at the given path
47 | *
48 | * path - path to file (relative to root or absolute)
49 | */
50 | async function fetchAndDecompressMapData (path) {
51 | const res = await fetch(path, {
52 | method: 'GET',
53 | headers: {
54 | Origin: 'https://mkv25.net'
55 | }
56 | })
57 | const arrayBuffer = await res.arrayBuffer()
58 | const dataArray = new Uint8Array(arrayBuffer)
59 |
60 | // inflate data
61 | const data = pako.inflate(dataArray)
62 | const result = new DataView(data.buffer)
63 |
64 | return result
65 | }
66 |
67 | /**
68 | * Fetches and parses a data file from a specific network path
69 | *
70 | * This only parses the data, it does NOT populate the image files.
71 | * These are generated and cached on-the-fly.
72 | *
73 | * NOTE: Due to CORS the desired file must be under the same origin.
74 | * path - path to data file
75 | *
76 | * @param {string} path remote URL of the .fdf-map file to fetch
77 | *
78 | * @return {MapData} a mapData object that's ready to have its layers queried with mapData.getLayer()
79 | */
80 | export async function loadMapFromURL (path) {
81 | const mapData = new MapData()
82 | try {
83 | const loadedData = await fetchAndDecompressMapData(path)
84 | mapData.parse(loadedData)
85 | } catch (err) {
86 | console.error(err)
87 | }
88 |
89 | return mapData
90 | }
91 |
92 | export default {
93 | loadMapFromURL,
94 | loadMapFromFileSystem
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/viewState/zoom-to.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Configure methods by injecting in shared viewState dependency
3 | *
4 | * @param {object} viewState
5 | */
6 | export default (viewState) => {
7 | /**
8 | * Zoom to a POI
9 | *
10 | * Side effects:
11 | * - read viewState.dfMapData for tileWidth, tileHeight, and mapData
12 | * - read viewState for idx, scale,
13 | * imgWidth, originalImgWidth,
14 | * imgHeight, originalImgHeight,
15 | * canvasWidth, canvasHeight
16 | * - calls console.log
17 | * - assigns viewState.idx, viewState.scale, viewState.imgWidth, viewState.imgHeight
18 | * viewState.imageX, viewState.imageY
19 | *
20 | * @param {integer} layer the layer to zoom to
21 | * @param {number} pscale the zoom scale (zoom amount)
22 | * @param {integer} xTile the xTile to zoom to
23 | * @param {integer} yTile the yTile to zoom to
24 | *
25 | * @return null manipulates viewState as a side effect
26 | */
27 | return (layer, pscale, xTile, yTile) => {
28 | // find the desired layer
29 | let found = false
30 | let curLayer
31 |
32 | const emptyMap = {}
33 | const dfMapData = viewState.dfMapData || emptyMap
34 | const { mapData } = dfMapData
35 | const { canvasWidth, canvasHeight } = viewState
36 |
37 | viewState.messages.push(`Canvas width: ${canvasWidth}, height: ${canvasHeight}`)
38 |
39 | if (!mapData) {
40 | viewState.messages.push(`Map data not available: ${JSON.stringify(dfMapData)}`)
41 | return
42 | }
43 |
44 | for (let i = 0; i < mapData.length; i++) {
45 | curLayer = mapData[i]
46 | if (curLayer.depth === layer) {
47 | found = true
48 | break
49 | }
50 | }
51 |
52 | if (!found) {
53 | viewState.messages.push(`Layer ${layer} not found in map layers: [${mapData.map(m => (m || {}).depth || -1)}]`)
54 | if (mapData.length > 0) {
55 | curLayer = mapData[0]
56 | viewState.messages.push(`Defaulting to first layer: ${curLayer.depth}`)
57 | }
58 | } else {
59 | viewState.messages.push(`Layer ${layer} found in map layers: [${mapData.map(m => (m || {}).depth || -1)}]`)
60 | }
61 |
62 | viewState.idx = curLayer.index
63 | viewState.scale = pscale
64 |
65 | viewState.imgWidth = viewState.originalImgWidth * viewState.scale
66 | viewState.imgHeight = viewState.originalImgHeight * viewState.scale
67 |
68 | console.log('[Zoom To]', { windowWidth: canvasWidth, tileWidth: dfMapData.tileWidth, scale: viewState.scale, xTile })
69 | viewState.imageX = canvasWidth / 2 - dfMapData.tileWidth * viewState.scale * xTile + dfMapData.tileWidth / 2 * viewState.scale
70 | viewState.imageY = canvasHeight / 2 - dfMapData.tileHeight * viewState.scale * yTile + dfMapData.tileHeight / 2 * viewState.scale
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/user-inputs.js:
--------------------------------------------------------------------------------
1 | import p5adapter from '../adapters/p5adapter.js'
2 |
3 | function isMouseOutOfBounds ({ mouseX, mouseY }, viewState) {
4 | return mouseX < 0 || mouseX > viewState.canvasWidth ||
5 | mouseY < 0 || mouseY > viewState.canvasHeight
6 | }
7 |
8 | function registerOn (browserWindow, { viewState }) {
9 | const p5 = p5adapter(browserWindow)
10 | /**
11 | * P5 MousePressed
12 | *
13 | * Called whenever the mouse is pressed
14 | */
15 | function mousePressed () {
16 | if (isMouseOutOfBounds(browserWindow, viewState)) {
17 | return
18 | }
19 |
20 | console.log('Mouse Pressed, viewState:', viewState)
21 |
22 | viewState.clickX = browserWindow.mouseX
23 | viewState.clickY = browserWindow.mouseY
24 | viewState.dragActive = true
25 | }
26 |
27 | /**
28 | * P5 MouseReleased
29 | *
30 | * Called whenever the mouse is released
31 | */
32 | function mouseReleased () {
33 | viewState.dragActive = false
34 | }
35 |
36 | /**
37 | * P5 MouseDragged
38 | *
39 | * Called whenever the mouse is dragged
40 | */
41 | function mouseDragged () {
42 | if (!viewState.dragActive) {
43 | return
44 | }
45 |
46 | const xDif = (browserWindow.mouseX - viewState.clickX)
47 | const yDif = (browserWindow.mouseY - viewState.clickY)
48 | viewState.clickX = browserWindow.mouseX
49 | viewState.clickY = browserWindow.mouseY
50 | viewState.imageX += xDif
51 | viewState.imageY += yDif
52 | }
53 |
54 | /**
55 | * P5 KeyPressed function
56 | *
57 | * called whenever a key is pressed
58 | */
59 | function keyPressed () {
60 | if (browserWindow.key === ',' || browserWindow.key === '<') {
61 | viewState.idx++
62 | if (viewState.idx >= viewState.dfMapData.numLayers) {
63 | viewState.idx = viewState.dfMapData.numLayers - 1
64 | }
65 | }
66 | if (browserWindow.key === '.' || browserWindow.key === '>') {
67 | viewState.idx = Math.max(0, viewState.idx - 1)
68 | }
69 |
70 | let modifier = 1
71 |
72 | if (p5.keyIsDown(90)) { // 'z'
73 | modifier = 10
74 | }
75 |
76 | if (p5.keyIsDown(102)) { // number 6
77 | viewState.imageX -= viewState.dfMapData.tileWidth * viewState.scale * modifier
78 | }
79 | if (p5.keyIsDown(100)) { // number 4
80 | viewState.imageX += viewState.dfMapData.tileWidth * viewState.scale * modifier
81 | }
82 | if (p5.keyIsDown(98)) { // number 2
83 | viewState.imageY -= viewState.dfMapData.tileHeight * viewState.scale * modifier
84 | }
85 | if (p5.keyIsDown(104)) { // number 8
86 | viewState.imageY += viewState.dfMapData.tileHeight * viewState.scale * modifier
87 | }
88 | }
89 |
90 | browserWindow.keyPressed = keyPressed
91 | browserWindow.mouseDragged = mouseDragged
92 | browserWindow.mousePressed = mousePressed
93 | browserWindow.mouseReleased = mouseReleased
94 | }
95 |
96 | export default registerOn
97 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # DFMA HTML5 Map Viewer Architecture
2 |
3 | The following diagram describes the intended architecture of the viewer:
4 |
5 | 
6 |
7 | The viewer is designed to run on a local user device; using web standard software such as a Web Browser supporting HTML5 DOM, Canvas, and JavaScript.
8 |
9 | Information about the map to view, and the binary map file, are stored on the DFMA website. The information about the map is encoded in a HTML document, on request, and sent to the user's browser. The binary .fdf-map file will be downloaded later. The map viewer itself is a packaged JS application put together using Parcel, P5 for pixel rendering to Canvas, and Pako for ZLIB decompression of the binary map file.
10 |
11 | ## Key Javascript Components
12 |
13 | To help navigate the code base, look out for these files:
14 | - Control Layer (map-viewer.js) - the control logic that holds information about the active map, active layer, view position, and loading state
15 | - Map Parser (parser.js) - the logic of decoding a binary .fdf-map file into useful JS memory structures
16 | - Map Renderer (renderer.js) - the P5 functions that render the pixels to screen
17 | - User Inputs (user-inputs.js) - event handlers for key presses, button clicks, and mouse movements
18 | - Read Map Info (read-map-info.js) - adapt HTML tags in the page to setup the initial map state
19 | - Drag & Drop (drag-and-drop.js) - legacy support for dragging a local map into the viewer
20 |
21 | ## JavaScript Libraries
22 |
23 | The project depends on these external libraries:
24 | - **p5.js** - aka Processing (https://p5js.org/) a JavaScript library for creative coding, with a focus on making coding accessible and inclusive for artists, designers, educators, beginners, and anyone else
25 | - **pako** (https://github.com/nodeca/pako) - a zlib port to javascript, very fast!
26 |
27 | ## Interface between HTML Document and Viewer
28 |
29 | The viewer needs to be included into the HTML document, like so:
30 | ```html
31 |
32 | ```
33 |
34 | The pixels from the viewer need somewhere to live in the HTML page, at the moment that's here:
35 | ```html
36 |
37 |
38 |
39 | ```
40 |
41 | To set the initial state of the viewer, the following tags should be present:
42 | ```html
43 |
44 | /dfma/maps/2020-03/mounf-Bellsshower-A_library-251-110.fdf-map
45 |
46 | What's the story of this fortress?
47 | 68
48 |
49 | 1824
50 |
51 | 976
52 |
53 | 1.00
54 |
55 | top
56 |
57 | Temporary fort
58 |
59 | What happened here?
60 | DFMA User ID
61 |
62 | ```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DFMA HTML 5 Map Viewer
2 |
3 | Home for development of a new HTML5 Map Viewer for Dwarf Fortress.
4 |
5 | Please read the [architecture page](./docs/README.md) for details about how the viewer works; and how to set it up in a larger project.
6 |
7 | ## Local Developer Setup
8 |
9 | Clone this repo, then run:
10 |
11 | ```
12 | npm install
13 | ```
14 |
15 | To start the parcel server, and view the map viewer, run:
16 |
17 | ```
18 | npm start
19 | open http://localhost:1234/index.html
20 | ```
21 |
22 | ## Testing
23 |
24 | When developing locally; please run:
25 | ```
26 | npm run lint && npm test -s
27 | ```
28 |
29 | These tests will be run on commit, and as part of PR testing.
30 |
31 | There are also integration tests that can run locally - they use a headless browser and perform a pixel diff of the rendered map with known good state.
32 |
33 | The tests can be run using:
34 |
35 | ```
36 | npm run test:integration
37 | ```
38 |
39 | 
40 |
41 | ## PR / Contribution Guidelines
42 |
43 | We have a [project page](https://github.com/orgs/df-map-archive/projects/1) for site wide improvements that you may be able to help with, and a [forum post](http://www.bay12forums.com/smf/index.php?topic=176276.0) that you can follow to discuss ideas. Otherwise [raise an issue](https://github.com/df-map-archive/dfma-html5-map-viewer/issues/new) on this repo and we'll get in touch.
44 |
45 | Github actions are set up to run linting and tests against branches and PRs. You can and should run tests locally before submitting a PR.
46 |
47 | When forking, or raising a PR please keep the PR in Draft until you're ready to merge, the project team will be notified in order to provide a code review before merging to master.
48 |
49 | Please ensure that tests pass; and that you've added adequate testing around any new functionality introduced to the code base.
50 |
51 | ## Deployment Pipeline
52 |
53 | The master branch is protected; the default action for merge to master is to build and deploy the viewer to the [/xdfmadev/parcel/](https://mkv25.net/dfma/xdfmadev/parcel/index.html) folder on the DFMA website.
54 |
55 | ## Creating a release
56 |
57 | When you have a working tested, ready to release, version of the map viewer...
58 |
59 | - [x] Update the version number in package.json using [`semver`](https://semver.org/)
60 | - [x] Commit with the message `Prepare v{major}.{minor}.{patch} for release`
61 | - [x] Go and draft a [new release](https://github.com/df-map-archive/dfma-html5-map-viewer/releases/new) with the tag in the syntax `v{major}.{minor}.{patch}`
62 | - [x] Use the tag version as the title i.e. `v{major}.{minor}.{patch}`
63 | - [x] Add a short and informative description about the release
64 | - [x] Submit the `Publish release` button
65 |
66 | This will trigger a deployment pipeline to upload the compiled files to:
67 | - `https://mkv25.net/dfma/viewer/js/$RELEASE_VERSION`
68 |
69 | ## Credits
70 |
71 | Credit where credit due:
72 | - Thanks to [Jacob Blomquist](https://github.com/JacobRBlomquist) for his [original and continued work](https://github.com/JacobRBlomquist/DFMA-Viewer-HTML5) on the HTML version of the map viewer
73 | - [p5.js](https://p5js.org/) - for a fantastic canvas interface
74 | - [pako](https://github.com/nodeca/pako) - for a fantastic zlib implementation
75 |
76 | ## Contributions
77 |
78 | - [Jacob Blomquist](https://github.com/JacobRBlomquist) - original implementation of a HTML based decoder and renderer
79 | - [John Beech](https://github.com/johnbeech) - refactor and redesign to work with DFMA (original author of historic Flash viewer)
80 | - [Max Heisinger](https://github.com/maximaximal) - remove `pixelDensity` call to support high-resolution fonts
81 |
--------------------------------------------------------------------------------
/src/static/main.css:
--------------------------------------------------------------------------------
1 | body{
2 | margin:0;
3 | padding:0;
4 | border:0;
5 | background-color: #aaa;
6 | /* reset */
7 | font-family: 'Mukta', sans-serif;
8 | }
9 |
10 | /* https://www.w3schools.com/Css/css_navbar_horizontal.asp*/
11 |
12 | #nav{
13 | list-style-type: none;
14 | margin:0;
15 | padding:0;
16 | overflow: hidden;
17 | background-color: #333;
18 | }
19 |
20 | #nav li{
21 | float:left;
22 | }
23 |
24 | #nav li a{
25 | display:block;
26 | color:white;
27 | text-align: center;
28 | padding:14px 16px;
29 | text-decoration: none;
30 | }
31 |
32 | #nav li a:hover{
33 | background-color: #111;
34 | }
35 |
36 | .container{
37 | margin:0 auto;
38 | width:800px;
39 | padding:0px 20px;
40 | background-color: white;
41 | }
42 | h1{
43 | padding:0px;
44 | margin:0;
45 | }
46 |
47 | /* https://meta.superuser.com/questions/4788/css-for-the-new-kbd-style */
48 | kbd
49 | {
50 | -moz-border-radius:3px;
51 | -moz-box-shadow:0 1px 0 rgba(0,0,0,0.2),0 0 0 2px #fff inset;
52 | -webkit-border-radius:3px;
53 | -webkit-box-shadow:0 1px 0 rgba(0,0,0,0.2),0 0 0 2px #fff inset;
54 | background-color:#f7f7f7;
55 | border:1px solid #ccc;
56 | border-radius:3px;
57 | box-shadow:0 1px 0 rgba(0,0,0,0.2),0 0 0 2px #fff inset;
58 | color:#333;
59 | display:inline-block;
60 | font-family:Arial,Helvetica,sans-serif;
61 | font-size:11px;
62 | line-height:1.4;
63 | margin:0 .1em;
64 | padding:.1em .6em;
65 | text-shadow:0 1px 0 #fff;
66 | }
67 | /* https://css-tricks.com/css3-progress-bars/ */
68 | #progressBar{
69 | height:20px;
70 | position: relative;
71 | background-color: #555;
72 | -moz-border-radius: 25px;
73 | -webkit-border-radius: 25px;
74 | border-radius: 25px;
75 | padding: 10px;
76 | box-shadow: inset 0 -1px 1px rgba(255,255,255,0.3);
77 | margin:5px;
78 | }
79 |
80 | #progressBar > span{
81 | display: block;
82 | height: 100%;
83 | border-top-right-radius: 8px;
84 | border-bottom-right-radius: 8px;
85 | border-top-left-radius: 20px;
86 | border-bottom-left-radius: 20px;
87 | background-color: rgb(43,194,83);
88 | background-image: linear-gradient(
89 | center bottom,
90 | rgb(43,194,83) 37%,
91 | rgb(84,240,84) 69%
92 | );
93 | box-shadow:
94 | inset 0 2px 9px rgba(255,255,255,0.3),
95 | inset 0 -2px 6px rgba(0,0,0,0.4);
96 | position: relative;
97 | overflow: hidden;
98 | }
99 |
100 | map-link {
101 | cursor: pointer
102 | }
103 |
104 | map-link:hover {
105 | color: #2266ff;
106 | }
107 |
108 | default-map {
109 | display: flex;
110 | flex-direction: column;
111 | border-radius: 0.5em;
112 | background: #eee;
113 | padding: 0.5em;
114 | }
115 |
116 | default-map > * {
117 | display: inline-block;
118 | flex: 1 1;
119 | }
120 |
121 | default-map > *::before {
122 | content: '?';
123 | display: inline-block;
124 | width: 150px;
125 | }
126 |
127 | map-link::before {
128 | content: 'Map Link:';
129 | }
130 |
131 | map-description::before {
132 | content: '';
133 | width: 20px;
134 | }
135 |
136 | start-level::before {
137 | content: 'Start Level:';
138 | }
139 |
140 | start-x::before {
141 | content: 'Start X:';
142 | }
143 |
144 | start-y::before {
145 | content: 'Start Y:';
146 | }
147 |
148 | start-zoom::before {
149 | content: 'Start Zoom:';
150 | }
151 |
152 | start-orientation::before {
153 | content: 'Start Orientation:';
154 | }
155 |
156 | poi-title::before {
157 | content: 'Point of Interest:';
158 | }
159 |
160 | poi-description::before {
161 | content: '';
162 | width: 20px;
163 | }
164 |
165 | poi-author::before {
166 | content: 'Author';
167 | }
--------------------------------------------------------------------------------
/src/components/parser.js:
--------------------------------------------------------------------------------
1 | import p5adapter from '../adapters/p5adapter.js'
2 |
3 | const browserWindow = (typeof window !== 'undefined') ? window : {}
4 | const p5 = p5adapter(browserWindow)
5 |
6 | /**
7 | * Object which stores all data for a specific map
8 | */
9 | export function MapData () {
10 | this.loaded = false
11 | this.version = 0
12 | this.numTiles = 0
13 | this.tileWidth = 0
14 | this.tileHeight = 0
15 | this.numTilesX = 0
16 | this.numTilesY = 0
17 | this.numLayers = 0
18 | this.tiles = []
19 | this.mapData = []
20 | this.horizontals = []
21 | this.verticals = []
22 | this.ptr = 0
23 | this.data = undefined
24 |
25 | /**
26 | * Get a specific layer's image (P5.image)
27 | */
28 | this.getLayer = function (idx) {
29 | const mData = this.mapData[idx]
30 | if (mData.loaded) {
31 | return mData.img
32 | } else {
33 | this.loadLayer(idx)
34 | return this.mapData[idx].img
35 | }
36 | }
37 |
38 | /**
39 | * **Internal Function***
40 | * Loads a layer if it is not already cached
41 | *
42 | * Why does javascript not have namespaces / private scope?
43 | * I know closures exist but they're like building a rocket to launch a
44 | * paper airplane.
45 | *
46 | * idx - Index of layer to load (not layer number)
47 | */
48 | this.loadLayer = function (idx) {
49 | if (this.mapData[idx].loaded) { return }
50 |
51 | this.mapData[idx].loading = true
52 | const curMapData = this.mapData[idx]
53 | const imgTWidth = curMapData.width
54 | const imgTHeight = curMapData.height
55 | this.numTilesX = imgTWidth
56 | this.numTilesY = imgTHeight
57 |
58 | const imgWidth = imgTWidth * this.tileWidth
59 | const imgHeight = imgTHeight * this.tileHeight
60 |
61 | const curLayer = p5.createImage(imgWidth, imgHeight)
62 | curLayer.loadPixels()
63 |
64 | // blit whole pix
65 | for (let x = 0; x < imgTWidth; x++) {
66 | for (let y = 0; y < imgTHeight; y++) {
67 | const yTileIndex = y * this.tileHeight
68 | const xTileIndex = x * this.tileWidth
69 | const curIndex = y + x * imgTHeight
70 |
71 | const curTile = curMapData.blocks[curIndex]
72 |
73 | for (let ty = 0; ty < this.tileHeight; ty++) {
74 | for (let tx = 0; tx < this.tileWidth; tx++) {
75 | const srcIdx = tx * 4 + ty * 4 * this.tileWidth
76 | const destIdx = ((xTileIndex + tx) * 4) + ((yTileIndex + ty) * 4 * imgWidth)
77 | curLayer.pixels[destIdx] = curTile[srcIdx]
78 | curLayer.pixels[destIdx + 1] = curTile[srcIdx + 1]
79 | curLayer.pixels[destIdx + 2] = curTile[srcIdx + 2]
80 | curLayer.pixels[destIdx + 3] = curTile[srcIdx + 3]
81 |
82 | // curLayer.set(xTileIndex + tx, yTileIndex + ty, color(curTile[srcIdx], curTile[srcIdx + 1], curTile[srcIdx + 2]))
83 | }
84 | }
85 | }
86 | }
87 |
88 | curLayer.updatePixels()
89 |
90 | this.mapData[idx].loaded = true
91 | this.mapData[idx].img = curLayer
92 | this.mapData[idx].loading = false
93 | }
94 |
95 | /**
96 | * Parse a dataview object built on a UInt8Array.
97 | *
98 | * Populates this object
99 | * data - Dataview object
100 | */
101 | this.parse = function (data) {
102 | if (this.loaded) {
103 | return
104 | }
105 | this.data = data
106 |
107 | this.version = data.getInt32(this.ptr, true)
108 | this.ptr += 4
109 | this.numTiles = data.getInt32(this.ptr, true)
110 | this.ptr += 4
111 | this.tileWidth = data.getInt32(this.ptr, true)
112 | this.ptr += 4
113 | this.tileHeight = data.getInt32(this.ptr, true)
114 | this.ptr += 4
115 | this.numLayers = data.getInt32(this.ptr, true)
116 | this.ptr += 4
117 |
118 | if (this.version < -3) {
119 | console.log('UNSUPPORTED FDF-MAP FILE FORMAT - Version: ' + this.version + ' IS INVALID OR NOT IMPLEMENTED YET')
120 | return
121 | }
122 |
123 | const flags = -1 - this.version
124 | // let RLE = false // Not using RLE
125 | let TID = false
126 |
127 | if (flags & 1) { TID = true }
128 | // if (flags & 2) { RLE = true }
129 |
130 | // get layer metadata
131 | for (let i = 0; i < this.numLayers; i++) {
132 | const curDepth = data.getInt32(this.ptr, true)
133 | this.ptr += 4
134 | const curWidth = data.getInt32(this.ptr, true)
135 | this.ptr += 4
136 | const curHeight = data.getInt32(this.ptr, true)
137 | this.ptr += 4
138 | this.mapData.push({ depth: curDepth, width: curWidth, height: curHeight, index: i, loaded: false, blocks: [] })
139 | }
140 |
141 | for (let curTileIdx = 0; curTileIdx < this.numTiles; curTileIdx++) {
142 | const numPixels = this.tileWidth * this.tileHeight
143 | let processed = 0
144 | const pixelData = []
145 |
146 | // throw away tile information for now
147 | if (TID) { this.ptr += 3 }
148 |
149 | while (processed < numPixels) { // P5 needs RGBA
150 | const num = data.getUint8(this.ptr++, true)
151 | const b = data.getUint8(this.ptr++, true)
152 | const g = data.getUint8(this.ptr++, true)
153 | const r = data.getUint8(this.ptr++, true)
154 | for (let i = num; i > 0; i--) {
155 | pixelData.push(r)// RED
156 | pixelData.push(g)// GREEN
157 | pixelData.push(b)// BLUE
158 | pixelData.push(255)// ALPHA
159 | processed++
160 | }
161 | }
162 | this.tiles.push(pixelData)
163 | }
164 |
165 | for (let i = 0; i < this.numLayers; i++) {
166 | const curMapData = this.mapData[i]
167 | const imgTWidth = curMapData.width
168 | const imgTHeight = curMapData.height
169 |
170 | for (let j = 0; j < imgTWidth * imgTHeight; j++) {
171 | let curIndex
172 | if (this.numTiles <= 127) {
173 | curIndex = this.data.getUint8(this.ptr++, true)
174 | } else if (this.numTiles <= 32767) {
175 | curIndex = this.data.getUint16(this.ptr, true)
176 | this.ptr += 2
177 | } else {
178 | curIndex = this.data.getUint32(this.ptr, true)
179 | this.ptr += 4
180 | }
181 | curMapData.blocks.push(this.tiles[curIndex])
182 | }
183 | }
184 |
185 | this.mapData.sort((a, b) => { // sort by layer
186 | return a.depth - b.depth
187 | })
188 |
189 | console.log('[Parser] Parsed', this.mapData.length, 'layers')
190 | this.loaded = true
191 | }
192 | }
193 |
194 | export default {
195 | MapData
196 | }
197 |
--------------------------------------------------------------------------------
/src/components/renderer.js:
--------------------------------------------------------------------------------
1 | import p5adapter from '../adapters/p5adapter.js'
2 |
3 | export default function ({ viewState, zoomIn, zoomOut }, browserWindow) {
4 | const p5 = p5adapter(browserWindow)
5 |
6 | // RENDERER SHARED STATE
7 |
8 | // CONSTANTS
9 |
10 | const { canvasWidth, canvasHeight } = viewState
11 |
12 | /**
13 | * P5 Preload.
14 | *
15 | * Occurs once before setup.
16 | */
17 | function preload () {
18 | // setMapByURL('file.fdf-map')
19 | }
20 |
21 | /**
22 | * P5 setup
23 | *
24 | * Occurs once before main loop
25 | */
26 | function setup () {
27 | const canvas = p5.createCanvas(canvasWidth, canvasHeight)
28 | canvas.parent('p5-dfma-html5-map-viewer')
29 | p5.background(0)
30 | p5.textFont('Helvetica', 15)
31 | p5.textAlign(browserWindow.CENTER, browserWindow.CENTER)
32 | }
33 |
34 | function drawMessages ({ messages }) {
35 | const textLeftOffset = 15
36 | const textTopOffset = 20
37 |
38 | const messagesToShow = messages.slice(Math.max(messages.length - 10, 0))
39 | if (messagesToShow.length === 0) {
40 | messagesToShow.push('No messages to show')
41 | }
42 | messagesToShow.forEach((line, index) => {
43 | const lineNo = Math.max(messages.length - 10, 0) + index
44 | p5.text(`${lineNo}: ${line}`, textLeftOffset, textTopOffset + 60 + (index * 20))
45 | })
46 | }
47 |
48 | /**
49 | * P5 Draw Function
50 | *
51 | * Occurs each frame
52 | */
53 | function draw () {
54 | if (viewState.dfMapData && viewState.dfMapData.loaded) {
55 | if (browserWindow.keyIsPressed === true) {
56 | if (browserWindow.key === '+' || browserWindow.key === '=') {
57 | zoomIn()
58 | }
59 |
60 | if (browserWindow.key === '-' || browserWindow.key === '_') {
61 | zoomOut()
62 | }
63 | }
64 |
65 | // setup zoom information
66 | if (viewState.originalImgWidth === 0) { // not loaded
67 | viewState.originalImgWidth = viewState.dfMapData.mapData[0].width * viewState.dfMapData.tileWidth
68 | viewState.originalImgHeight = viewState.dfMapData.mapData[0].height * viewState.dfMapData.tileHeight
69 | viewState.imgWidth = viewState.originalImgWidth * viewState.scale
70 | viewState.imgHeight = viewState.originalImgHeight * viewState.scale
71 | // return;
72 | }
73 |
74 | p5.background(0) // Make background black
75 |
76 | const mapData = viewState.dfMapData.mapData
77 | if (mapData[viewState.idx] !== undefined && mapData[viewState.idx].loaded === false && !mapData[viewState.idx].loading) {
78 | viewState.dfMapData.getLayer(viewState.idx)
79 | return
80 | }
81 | if (mapData[viewState.idx] === undefined || mapData[viewState.idx].img === undefined) {
82 | return
83 | }
84 | const img = viewState.dfMapData.getLayer(viewState.idx)
85 | p5.image(img, viewState.imageX, viewState.imageY, viewState.imgWidth, viewState.imgHeight)
86 |
87 | const selectorWidth = viewState.dfMapData.tileWidth * viewState.scale
88 | const selectorHeight = viewState.dfMapData.tileHeight * viewState.scale
89 |
90 | // draw selector
91 | const curCenterX = canvasWidth / 2 - viewState.imageX
92 | const curCenterY = canvasHeight / 2 - viewState.imageY
93 | const selectedX = p5.floor(curCenterX / (viewState.dfMapData.tileWidth * viewState.scale))
94 | const selectedY = p5.floor(curCenterY / (viewState.dfMapData.tileHeight * viewState.scale))
95 |
96 | p5.stroke(255, 0, 0)
97 | p5.strokeWeight(p5.max(viewState.scale, 2))
98 | p5.noFill()
99 | p5.rect(viewState.imageX + selectorWidth * selectedX, viewState.imageY + selectorHeight * selectedY, selectorWidth, selectorHeight)
100 | p5.stroke(255, 255, 0)
101 | const crosshairSize = 5
102 | p5.strokeWeight(2)
103 | p5.line(canvasWidth / 2 - crosshairSize, canvasHeight / 2, canvasWidth / 2 + crosshairSize, canvasHeight / 2)
104 | p5.line(canvasWidth / 2, canvasHeight / 2 - crosshairSize, canvasWidth / 2, canvasHeight / 2 + crosshairSize)
105 |
106 | // text
107 | const drawText = !viewState.hideText
108 | if (drawText) {
109 | const textLeftOffset = 15
110 | const textTopOffset = 20
111 | p5.stroke(255)
112 | p5.noFill()
113 | p5.strokeWeight(1)
114 | p5.textFont('Helvetica', 12)
115 | p5.textAlign(browserWindow.LEFT)
116 | p5.text('Layer: ' + viewState.dfMapData.mapData[viewState.idx].depth, textLeftOffset, textTopOffset)
117 | p5.text('Zoom: ' + viewState.scale.toFixed(2), textLeftOffset, textTopOffset + 20)
118 | p5.text(`X: ${selectedX} ${viewState.imageX.toFixed(2)}, Y: ${selectedY} ${viewState.imageY.toFixed(2)}`, textLeftOffset, textTopOffset + 40)
119 |
120 | drawMessages(viewState)
121 | }
122 |
123 | if (viewState.showTiles) {
124 | showTiles()
125 | }
126 |
127 | if (viewState.dragged) {
128 | p5.fill(0, 0, 0, 200)
129 | p5.textFont('Helvetica', 30)
130 | p5.textAlign(browserWindow.CENTER, browserWindow.CENTER)
131 | p5.rect(0, 0, canvasWidth, canvasHeight)
132 | p5.fill(255)
133 | p5.text('DROP FDF-MAP FILE HERE', window.width / 2, window.height / 2)
134 | }
135 | } else {
136 | p5.background(0)
137 | p5.textFont('Helvetica', 20)
138 | p5.textAlign(browserWindow.CENTER, browserWindow.CENTER)
139 | p5.stroke(255)
140 | p5.fill(255)
141 |
142 | const loadingMessage = viewState.dfMapData ? 'Map Data Loaded...' : 'Loading... ' + Math.random().toFixed(4)
143 | p5.text(loadingMessage, canvasWidth / 2, canvasHeight / 2)
144 | }
145 | }
146 |
147 | function showTiles () {
148 | p5.loadPixels()
149 | let xT = 0; let yT = 0
150 | const wPixels = viewState.dfMapData.tileWidth
151 | const hPixels = viewState.dfMapData.tileHeight
152 | for (let i = 0; i < viewState.dfMapData.numTiles; i++) {
153 | const cols = viewState.dfMapData.tiles[i]
154 | for (let y = 0; y < hPixels; y++) {
155 | for (let x = 0; x < wPixels; x++) {
156 | const idx = x * 4 + y * 4 * wPixels
157 | browserWindow.pixels[(xT * wPixels * 4) + x * 4 + (y + yT * hPixels) * browserWindow.width * 4] = cols[idx]
158 | browserWindow.pixels[(xT * wPixels * 4) + x * 4 + (y + yT * hPixels) * browserWindow.width * 4 + 1] = cols[idx + 1]
159 | browserWindow.pixels[(xT * wPixels * 4) + x * 4 + (y + yT * hPixels) * browserWindow.width * 4 + 2] = cols[idx + 2]
160 | browserWindow.pixels[(xT * wPixels * 4) + x * 4 + (y + yT * hPixels) * browserWindow.width * 4 + 3] = cols[idx + 3]
161 | }
162 | }
163 | xT++
164 | if (xT >= viewState.width / wPixels) {
165 | xT = 0
166 | yT++
167 | if (yT >= viewState.height / hPixels) {
168 | break
169 | }
170 | }
171 | }
172 | p5.updatePixels()
173 | }
174 |
175 | return {
176 | draw,
177 | preload,
178 | setup
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/licenses/P5 LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 2.1, February 1999
3 |
4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc.
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | (This is the first released version of the Lesser GPL. It also counts
10 | as the successor of the GNU Library Public License, version 2, hence
11 | the version number 2.1.)
12 |
13 | Preamble
14 |
15 | The licenses for most software are designed to take away your
16 | freedom to share and change it. By contrast, the GNU General Public
17 | Licenses are intended to guarantee your freedom to share and change
18 | free software--to make sure the software is free for all its users.
19 |
20 | This license, the Lesser General Public License, applies to some
21 | specially designated software packages--typically libraries--of the
22 | Free Software Foundation and other authors who decide to use it. You
23 | can use it too, but we suggest you first think carefully about whether
24 | this license or the ordinary General Public License is the better
25 | strategy to use in any particular case, based on the explanations below.
26 |
27 | When we speak of free software, we are referring to freedom of use,
28 | not price. Our General Public Licenses are designed to make sure that
29 | you have the freedom to distribute copies of free software (and charge
30 | for this service if you wish); that you receive source code or can get
31 | it if you want it; that you can change the software and use pieces of
32 | it in new free programs; and that you are informed that you can do
33 | these things.
34 |
35 | To protect your rights, we need to make restrictions that forbid
36 | distributors to deny you these rights or to ask you to surrender these
37 | rights. These restrictions translate to certain responsibilities for
38 | you if you distribute copies of the library or if you modify it.
39 |
40 | For example, if you distribute copies of the library, whether gratis
41 | or for a fee, you must give the recipients all the rights that we gave
42 | you. You must make sure that they, too, receive or can get the source
43 | code. If you link other code with the library, you must provide
44 | complete object files to the recipients, so that they can relink them
45 | with the library after making changes to the library and recompiling
46 | it. And you must show them these terms so they know their rights.
47 |
48 | We protect your rights with a two-step method: (1) we copyright the
49 | library, and (2) we offer you this license, which gives you legal
50 | permission to copy, distribute and/or modify the library.
51 |
52 | To protect each distributor, we want to make it very clear that
53 | there is no warranty for the free library. Also, if the library is
54 | modified by someone else and passed on, the recipients should know
55 | that what they have is not the original version, so that the original
56 | author's reputation will not be affected by problems that might be
57 | introduced by others.
58 |
59 | Finally, software patents pose a constant threat to the existence of
60 | any free program. We wish to make sure that a company cannot
61 | effectively restrict the users of a free program by obtaining a
62 | restrictive license from a patent holder. Therefore, we insist that
63 | any patent license obtained for a version of the library must be
64 | consistent with the full freedom of use specified in this license.
65 |
66 | Most GNU software, including some libraries, is covered by the
67 | ordinary GNU General Public License. This license, the GNU Lesser
68 | General Public License, applies to certain designated libraries, and
69 | is quite different from the ordinary General Public License. We use
70 | this license for certain libraries in order to permit linking those
71 | libraries into non-free programs.
72 |
73 | When a program is linked with a library, whether statically or using
74 | a shared library, the combination of the two is legally speaking a
75 | combined work, a derivative of the original library. The ordinary
76 | General Public License therefore permits such linking only if the
77 | entire combination fits its criteria of freedom. The Lesser General
78 | Public License permits more lax criteria for linking other code with
79 | the library.
80 |
81 | We call this license the "Lesser" General Public License because it
82 | does Less to protect the user's freedom than the ordinary General
83 | Public License. It also provides other free software developers Less
84 | of an advantage over competing non-free programs. These disadvantages
85 | are the reason we use the ordinary General Public License for many
86 | libraries. However, the Lesser license provides advantages in certain
87 | special circumstances.
88 |
89 | For example, on rare occasions, there may be a special need to
90 | encourage the widest possible use of a certain library, so that it becomes
91 | a de-facto standard. To achieve this, non-free programs must be
92 | allowed to use the library. A more frequent case is that a free
93 | library does the same job as widely used non-free libraries. In this
94 | case, there is little to gain by limiting the free library to free
95 | software only, so we use the Lesser General Public License.
96 |
97 | In other cases, permission to use a particular library in non-free
98 | programs enables a greater number of people to use a large body of
99 | free software. For example, permission to use the GNU C Library in
100 | non-free programs enables many more people to use the whole GNU
101 | operating system, as well as its variant, the GNU/Linux operating
102 | system.
103 |
104 | Although the Lesser General Public License is Less protective of the
105 | users' freedom, it does ensure that the user of a program that is
106 | linked with the Library has the freedom and the wherewithal to run
107 | that program using a modified version of the Library.
108 |
109 | The precise terms and conditions for copying, distribution and
110 | modification follow. Pay close attention to the difference between a
111 | "work based on the library" and a "work that uses the library". The
112 | former contains code derived from the library, whereas the latter must
113 | be combined with the library in order to run.
114 |
115 | GNU LESSER GENERAL PUBLIC LICENSE
116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
117 |
118 | 0. This License Agreement applies to any software library or other
119 | program which contains a notice placed by the copyright holder or
120 | other authorized party saying it may be distributed under the terms of
121 | this Lesser General Public License (also called "this License").
122 | Each licensee is addressed as "you".
123 |
124 | A "library" means a collection of software functions and/or data
125 | prepared so as to be conveniently linked with application programs
126 | (which use some of those functions and data) to form executables.
127 |
128 | The "Library", below, refers to any such software library or work
129 | which has been distributed under these terms. A "work based on the
130 | Library" means either the Library or any derivative work under
131 | copyright law: that is to say, a work containing the Library or a
132 | portion of it, either verbatim or with modifications and/or translated
133 | straightforwardly into another language. (Hereinafter, translation is
134 | included without limitation in the term "modification".)
135 |
136 | "Source code" for a work means the preferred form of the work for
137 | making modifications to it. For a library, complete source code means
138 | all the source code for all modules it contains, plus any associated
139 | interface definition files, plus the scripts used to control compilation
140 | and installation of the library.
141 |
142 | Activities other than copying, distribution and modification are not
143 | covered by this License; they are outside its scope. The act of
144 | running a program using the Library is not restricted, and output from
145 | such a program is covered only if its contents constitute a work based
146 | on the Library (independent of the use of the Library in a tool for
147 | writing it). Whether that is true depends on what the Library does
148 | and what the program that uses the Library does.
149 |
150 | 1. You may copy and distribute verbatim copies of the Library's
151 | complete source code as you receive it, in any medium, provided that
152 | you conspicuously and appropriately publish on each copy an
153 | appropriate copyright notice and disclaimer of warranty; keep intact
154 | all the notices that refer to this License and to the absence of any
155 | warranty; and distribute a copy of this License along with the
156 | Library.
157 |
158 | You may charge a fee for the physical act of transferring a copy,
159 | and you may at your option offer warranty protection in exchange for a
160 | fee.
161 |
162 | 2. You may modify your copy or copies of the Library or any portion
163 | of it, thus forming a work based on the Library, and copy and
164 | distribute such modifications or work under the terms of Section 1
165 | above, provided that you also meet all of these conditions:
166 |
167 | a) The modified work must itself be a software library.
168 |
169 | b) You must cause the files modified to carry prominent notices
170 | stating that you changed the files and the date of any change.
171 |
172 | c) You must cause the whole of the work to be licensed at no
173 | charge to all third parties under the terms of this License.
174 |
175 | d) If a facility in the modified Library refers to a function or a
176 | table of data to be supplied by an application program that uses
177 | the facility, other than as an argument passed when the facility
178 | is invoked, then you must make a good faith effort to ensure that,
179 | in the event an application does not supply such function or
180 | table, the facility still operates, and performs whatever part of
181 | its purpose remains meaningful.
182 |
183 | (For example, a function in a library to compute square roots has
184 | a purpose that is entirely well-defined independent of the
185 | application. Therefore, Subsection 2d requires that any
186 | application-supplied function or table used by this function must
187 | be optional: if the application does not supply it, the square
188 | root function must still compute square roots.)
189 |
190 | These requirements apply to the modified work as a whole. If
191 | identifiable sections of that work are not derived from the Library,
192 | and can be reasonably considered independent and separate works in
193 | themselves, then this License, and its terms, do not apply to those
194 | sections when you distribute them as separate works. But when you
195 | distribute the same sections as part of a whole which is a work based
196 | on the Library, the distribution of the whole must be on the terms of
197 | this License, whose permissions for other licensees extend to the
198 | entire whole, and thus to each and every part regardless of who wrote
199 | it.
200 |
201 | Thus, it is not the intent of this section to claim rights or contest
202 | your rights to work written entirely by you; rather, the intent is to
203 | exercise the right to control the distribution of derivative or
204 | collective works based on the Library.
205 |
206 | In addition, mere aggregation of another work not based on the Library
207 | with the Library (or with a work based on the Library) on a volume of
208 | a storage or distribution medium does not bring the other work under
209 | the scope of this License.
210 |
211 | 3. You may opt to apply the terms of the ordinary GNU General Public
212 | License instead of this License to a given copy of the Library. To do
213 | this, you must alter all the notices that refer to this License, so
214 | that they refer to the ordinary GNU General Public License, version 2,
215 | instead of to this License. (If a newer version than version 2 of the
216 | ordinary GNU General Public License has appeared, then you can specify
217 | that version instead if you wish.) Do not make any other change in
218 | these notices.
219 |
220 | Once this change is made in a given copy, it is irreversible for
221 | that copy, so the ordinary GNU General Public License applies to all
222 | subsequent copies and derivative works made from that copy.
223 |
224 | This option is useful when you wish to copy part of the code of
225 | the Library into a program that is not a library.
226 |
227 | 4. You may copy and distribute the Library (or a portion or
228 | derivative of it, under Section 2) in object code or executable form
229 | under the terms of Sections 1 and 2 above provided that you accompany
230 | it with the complete corresponding machine-readable source code, which
231 | must be distributed under the terms of Sections 1 and 2 above on a
232 | medium customarily used for software interchange.
233 |
234 | If distribution of object code is made by offering access to copy
235 | from a designated place, then offering equivalent access to copy the
236 | source code from the same place satisfies the requirement to
237 | distribute the source code, even though third parties are not
238 | compelled to copy the source along with the object code.
239 |
240 | 5. A program that contains no derivative of any portion of the
241 | Library, but is designed to work with the Library by being compiled or
242 | linked with it, is called a "work that uses the Library". Such a
243 | work, in isolation, is not a derivative work of the Library, and
244 | therefore falls outside the scope of this License.
245 |
246 | However, linking a "work that uses the Library" with the Library
247 | creates an executable that is a derivative of the Library (because it
248 | contains portions of the Library), rather than a "work that uses the
249 | library". The executable is therefore covered by this License.
250 | Section 6 states terms for distribution of such executables.
251 |
252 | When a "work that uses the Library" uses material from a header file
253 | that is part of the Library, the object code for the work may be a
254 | derivative work of the Library even though the source code is not.
255 | Whether this is true is especially significant if the work can be
256 | linked without the Library, or if the work is itself a library. The
257 | threshold for this to be true is not precisely defined by law.
258 |
259 | If such an object file uses only numerical parameters, data
260 | structure layouts and accessors, and small macros and small inline
261 | functions (ten lines or less in length), then the use of the object
262 | file is unrestricted, regardless of whether it is legally a derivative
263 | work. (Executables containing this object code plus portions of the
264 | Library will still fall under Section 6.)
265 |
266 | Otherwise, if the work is a derivative of the Library, you may
267 | distribute the object code for the work under the terms of Section 6.
268 | Any executables containing that work also fall under Section 6,
269 | whether or not they are linked directly with the Library itself.
270 |
271 | 6. As an exception to the Sections above, you may also combine or
272 | link a "work that uses the Library" with the Library to produce a
273 | work containing portions of the Library, and distribute that work
274 | under terms of your choice, provided that the terms permit
275 | modification of the work for the customer's own use and reverse
276 | engineering for debugging such modifications.
277 |
278 | You must give prominent notice with each copy of the work that the
279 | Library is used in it and that the Library and its use are covered by
280 | this License. You must supply a copy of this License. If the work
281 | during execution displays copyright notices, you must include the
282 | copyright notice for the Library among them, as well as a reference
283 | directing the user to the copy of this License. Also, you must do one
284 | of these things:
285 |
286 | a) Accompany the work with the complete corresponding
287 | machine-readable source code for the Library including whatever
288 | changes were used in the work (which must be distributed under
289 | Sections 1 and 2 above); and, if the work is an executable linked
290 | with the Library, with the complete machine-readable "work that
291 | uses the Library", as object code and/or source code, so that the
292 | user can modify the Library and then relink to produce a modified
293 | executable containing the modified Library. (It is understood
294 | that the user who changes the contents of definitions files in the
295 | Library will not necessarily be able to recompile the application
296 | to use the modified definitions.)
297 |
298 | b) Use a suitable shared library mechanism for linking with the
299 | Library. A suitable mechanism is one that (1) uses at run time a
300 | copy of the library already present on the user's computer system,
301 | rather than copying library functions into the executable, and (2)
302 | will operate properly with a modified version of the library, if
303 | the user installs one, as long as the modified version is
304 | interface-compatible with the version that the work was made with.
305 |
306 | c) Accompany the work with a written offer, valid for at
307 | least three years, to give the same user the materials
308 | specified in Subsection 6a, above, for a charge no more
309 | than the cost of performing this distribution.
310 |
311 | d) If distribution of the work is made by offering access to copy
312 | from a designated place, offer equivalent access to copy the above
313 | specified materials from the same place.
314 |
315 | e) Verify that the user has already received a copy of these
316 | materials or that you have already sent this user a copy.
317 |
318 | For an executable, the required form of the "work that uses the
319 | Library" must include any data and utility programs needed for
320 | reproducing the executable from it. However, as a special exception,
321 | the materials to be distributed need not include anything that is
322 | normally distributed (in either source or binary form) with the major
323 | components (compiler, kernel, and so on) of the operating system on
324 | which the executable runs, unless that component itself accompanies
325 | the executable.
326 |
327 | It may happen that this requirement contradicts the license
328 | restrictions of other proprietary libraries that do not normally
329 | accompany the operating system. Such a contradiction means you cannot
330 | use both them and the Library together in an executable that you
331 | distribute.
332 |
333 | 7. You may place library facilities that are a work based on the
334 | Library side-by-side in a single library together with other library
335 | facilities not covered by this License, and distribute such a combined
336 | library, provided that the separate distribution of the work based on
337 | the Library and of the other library facilities is otherwise
338 | permitted, and provided that you do these two things:
339 |
340 | a) Accompany the combined library with a copy of the same work
341 | based on the Library, uncombined with any other library
342 | facilities. This must be distributed under the terms of the
343 | Sections above.
344 |
345 | b) Give prominent notice with the combined library of the fact
346 | that part of it is a work based on the Library, and explaining
347 | where to find the accompanying uncombined form of the same work.
348 |
349 | 8. You may not copy, modify, sublicense, link with, or distribute
350 | the Library except as expressly provided under this License. Any
351 | attempt otherwise to copy, modify, sublicense, link with, or
352 | distribute the Library is void, and will automatically terminate your
353 | rights under this License. However, parties who have received copies,
354 | or rights, from you under this License will not have their licenses
355 | terminated so long as such parties remain in full compliance.
356 |
357 | 9. You are not required to accept this License, since you have not
358 | signed it. However, nothing else grants you permission to modify or
359 | distribute the Library or its derivative works. These actions are
360 | prohibited by law if you do not accept this License. Therefore, by
361 | modifying or distributing the Library (or any work based on the
362 | Library), you indicate your acceptance of this License to do so, and
363 | all its terms and conditions for copying, distributing or modifying
364 | the Library or works based on it.
365 |
366 | 10. Each time you redistribute the Library (or any work based on the
367 | Library), the recipient automatically receives a license from the
368 | original licensor to copy, distribute, link with or modify the Library
369 | subject to these terms and conditions. You may not impose any further
370 | restrictions on the recipients' exercise of the rights granted herein.
371 | You are not responsible for enforcing compliance by third parties with
372 | this License.
373 |
374 | 11. If, as a consequence of a court judgment or allegation of patent
375 | infringement or for any other reason (not limited to patent issues),
376 | conditions are imposed on you (whether by court order, agreement or
377 | otherwise) that contradict the conditions of this License, they do not
378 | excuse you from the conditions of this License. If you cannot
379 | distribute so as to satisfy simultaneously your obligations under this
380 | License and any other pertinent obligations, then as a consequence you
381 | may not distribute the Library at all. For example, if a patent
382 | license would not permit royalty-free redistribution of the Library by
383 | all those who receive copies directly or indirectly through you, then
384 | the only way you could satisfy both it and this License would be to
385 | refrain entirely from distribution of the Library.
386 |
387 | If any portion of this section is held invalid or unenforceable under any
388 | particular circumstance, the balance of the section is intended to apply,
389 | and the section as a whole is intended to apply in other circumstances.
390 |
391 | It is not the purpose of this section to induce you to infringe any
392 | patents or other property right claims or to contest validity of any
393 | such claims; this section has the sole purpose of protecting the
394 | integrity of the free software distribution system which is
395 | implemented by public license practices. Many people have made
396 | generous contributions to the wide range of software distributed
397 | through that system in reliance on consistent application of that
398 | system; it is up to the author/donor to decide if he or she is willing
399 | to distribute software through any other system and a licensee cannot
400 | impose that choice.
401 |
402 | This section is intended to make thoroughly clear what is believed to
403 | be a consequence of the rest of this License.
404 |
405 | 12. If the distribution and/or use of the Library is restricted in
406 | certain countries either by patents or by copyrighted interfaces, the
407 | original copyright holder who places the Library under this License may add
408 | an explicit geographical distribution limitation excluding those countries,
409 | so that distribution is permitted only in or among countries not thus
410 | excluded. In such case, this License incorporates the limitation as if
411 | written in the body of this License.
412 |
413 | 13. The Free Software Foundation may publish revised and/or new
414 | versions of the Lesser General Public License from time to time.
415 | Such new versions will be similar in spirit to the present version,
416 | but may differ in detail to address new problems or concerns.
417 |
418 | Each version is given a distinguishing version number. If the Library
419 | specifies a version number of this License which applies to it and
420 | "any later version", you have the option of following the terms and
421 | conditions either of that version or of any later version published by
422 | the Free Software Foundation. If the Library does not specify a
423 | license version number, you may choose any version ever published by
424 | the Free Software Foundation.
425 |
426 | 14. If you wish to incorporate parts of the Library into other free
427 | programs whose distribution conditions are incompatible with these,
428 | write to the author to ask for permission. For software which is
429 | copyrighted by the Free Software Foundation, write to the Free
430 | Software Foundation; we sometimes make exceptions for this. Our
431 | decision will be guided by the two goals of preserving the free status
432 | of all derivatives of our free software and of promoting the sharing
433 | and reuse of software generally.
434 |
435 | NO WARRANTY
436 |
437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
446 |
447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
456 | DAMAGES.
457 |
458 | END OF TERMS AND CONDITIONS
--------------------------------------------------------------------------------
/docs/diagrams/architecture.drawio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------