From a43fe456ade71c510524fc4aaa80ca4c7bde207f Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Mon, 26 Dec 2022 09:14:19 -0300 Subject: [PATCH 1/4] Initial code --- .gitignore | 2 - src/lib/common.ts | 1 + src/render/map.ts | 5 +- src/render/tile.ts | 134 +++++++++++++++++- .../10_Atlas_strikethrough_tiles.stories.tsx | 93 ++++++++++++ 5 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 stories/10_Atlas_strikethrough_tiles.stories.tsx diff --git a/.gitignore b/.gitignore index c39c435..404d351 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ *.d.ts -*.css lib node_modules -lib diff --git a/src/lib/common.ts b/src/lib/common.ts index 70085c8..9055301 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -11,4 +11,5 @@ export type Tile = { left?: boolean topLeft?: boolean scale?: number + strikethrough?: boolean } diff --git a/src/render/map.ts b/src/render/map.ts index 4175128..3b77776 100644 --- a/src/render/map.ts +++ b/src/render/map.ts @@ -29,7 +29,7 @@ export function renderMap(args: { if (!tile) { continue } - const { color, top, left, topLeft, scale } = tile + const { color, top, left, topLeft, scale, strikethrough } = tile const halfSize = scale ? (size * scale) / 2 : size / 2 @@ -44,7 +44,8 @@ export function renderMap(args: { left, top, topLeft, - scale + scale, + strikethrough }) } } diff --git a/src/render/tile.ts b/src/render/tile.ts index e845917..5b96936 100644 --- a/src/render/tile.ts +++ b/src/render/tile.ts @@ -1,3 +1,67 @@ +function strikethroughTile( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + size: number, + position?: { + top?: boolean, + left?: boolean, + topLeft?: boolean + } +) { + ctx.save() + ctx.beginPath() + ctx.lineWidth = 1 + ctx.lineCap = 'square' + // This is to make it centered, but I'm not that we need it really + // We need to make sure that this works correctly. + let clipX = x + let clipY = y + let clipSize = size + if (!position?.top && !position?.left) { + clipX += 1 + clipY += 1 + clipSize -= 2 + } else if (position?.top && position?.left && position?.topLeft) { + clipSize -= + 2 + } else { + if (position?.left) { + clipY -= 4 + } else if (position?.top) { + + } + } + // Clipping rectangle + // ctx.fillRect(x, y, x + size, y + size) + ctx.moveTo(clipX, clipY) + ctx.lineTo(clipX, clipY + clipSize) + ctx.lineTo(clipX + clipSize, clipY + clipSize) + ctx.lineTo(clipX + clipSize, clipY) + ctx.lineTo(clipX, clipY) + ctx.closePath() + ctx.clip() + + const color1 = '#000000' + const numberOfStripes = 6 + const thickness = size / numberOfStripes + ctx.globalAlpha = 0.3 + + for (var i = 0; i < numberOfStripes * 2; i++) { + if (i % 2 !== 0) { + continue + } + ctx.beginPath() + ctx.strokeStyle = color1 + ctx.lineWidth = thickness / 1.5 + ctx.lineCap = 'square' + ctx.moveTo(x + i * thickness + thickness / 2 - size, y) + ctx.lineTo(x + i * thickness + thickness / 2, y + size) + ctx.stroke() + } + + ctx.restore() +} + export function renderTile(args: { ctx: CanvasRenderingContext2D x: number @@ -10,8 +74,22 @@ export function renderTile(args: { top?: boolean topLeft?: boolean scale?: number + strikethrough?: boolean }) { - const { ctx, x, y, size, padding, offset, color, left, top, topLeft, scale } = args + const { + ctx, + x, + y, + size, + padding, + offset, + color, + left, + top, + topLeft, + scale, + strikethrough + } = args ctx.fillStyle = color @@ -25,6 +103,20 @@ export function renderTile(args: { tileSize - padding, tileSize - padding ) + + if (strikethrough) { + strikethroughTile( + ctx, + x - tileSize + padding, + y - tileSize + padding, + tileSize - padding, + { + top, + left, + topLeft + } + ) + } } else if (top && left && topLeft) { // connected everywhere: it's a square ctx.fillRect( @@ -33,6 +125,20 @@ export function renderTile(args: { tileSize + offset, tileSize + offset ) + + if (strikethrough) { + strikethroughTile( + ctx, + x - tileSize - offset, + y - tileSize - offset, + tileSize + offset, + { + top, + left, + topLeft + } + ) + } } else { if (left) { // connected left: it's a rectangle @@ -42,6 +148,19 @@ export function renderTile(args: { tileSize + offset, tileSize - padding ) + if (strikethrough) { + strikethroughTile( + ctx, + x - tileSize - offset, + y - tileSize + padding, + tileSize + offset, + { + top, + left, + topLeft + } + ) + } } if (top) { // connected top: it's a rectangle @@ -51,6 +170,19 @@ export function renderTile(args: { tileSize - padding, tileSize + offset ) + if (strikethrough) { + strikethroughTile( + ctx, + x - tileSize + padding, + y - tileSize - offset, + tileSize - padding, + { + top, + left, + topLeft + } + ) + } } } } diff --git a/stories/10_Atlas_strikethrough_tiles.stories.tsx b/stories/10_Atlas_strikethrough_tiles.stories.tsx new file mode 100644 index 0000000..80312b7 --- /dev/null +++ b/stories/10_Atlas_strikethrough_tiles.stories.tsx @@ -0,0 +1,93 @@ +import * as React from 'react' + +import { storiesOf } from '@storybook/react' +// @ts-ignore +import { withKnobs } from '@storybook/addon-knobs' + +import { TileMap, Layer } from '../src' +import { Tile } from '../src/lib/common' + +import './stories.css' + +const layer = new Map>() +layer.set(0, new Map()) +layer.set(2, new Map()) +layer.set(3, new Map()) +layer.set(4, new Map()) +layer.set(5, new Map()) + +// Single tile +layer.get(0)?.set(0, { + color: '#ff9990', + strikethrough: true +}) + +// Vertical rectangle +for(let i = 2; i < 5; i++) { + layer.get(0)?.set(i, { + color: '#ff9990', + top: i !== 4, + strikethrough: true + }) +} + +// Horizontal rectangle +for(let i = 2; i < 5; i++) { + layer.get(i)?.set(0, { + color: '#ff9990', + left: i !== 2, + strikethrough: true + }) +} + +// Square +for(let i = 2; i < 4; i++) { + for(let j = 2; j < 4; j++) { + layer.get(i)?.set(j, { + color: '#ff9990', + left: i === 3, + top: j === 2, + strikethrough: true + }) + } +} + +// Tetris brick +layer.get(2)?.set(-2, { + color: '#ff9990', + strikethrough: true +}) +layer.get(2)?.set(-3, { + color: '#ff9990', + topLeft: true, + top: true, + strikethrough: true +}) +layer.get(3)?.set(-3, { + color: '#ff9990', + left: true, + strikethrough: true +}) +layer.get(3)?.set(-4, { + color: '#ff9990', + top: true, + strikethrough: true +}) + +const chessboardLayer: Layer = (x, y) => { + if (!layer.get(x)?.get(y)) { + return { + color: '#888888' + } + } + + return layer.get(x)!.get(y)! +} + +const stories = storiesOf('TileMap', module) + +stories.addDecorator(withKnobs) + +stories.add('10. Strikethrough layer', () => { + return +}) From ee436ce48757a3c357370e9cbf676e6ce0021bbf Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Fri, 30 Dec 2022 10:14:31 -0300 Subject: [PATCH 2/4] fix: Improve strikethrough performance --- src/components/TileMap/TileMap.tsx | 49 ++-- src/components/TileMap/TileMap.types.ts | 1 + src/render/map.ts | 45 +++- src/render/tile.ts | 222 ++++++++---------- .../10_Atlas_strikethrough_tiles.stories.tsx | 93 +++++--- 5 files changed, 234 insertions(+), 176 deletions(-) diff --git a/src/components/TileMap/TileMap.tsx b/src/components/TileMap/TileMap.tsx index 04ac68e..f8d406c 100644 --- a/src/components/TileMap/TileMap.tsx +++ b/src/components/TileMap/TileMap.tsx @@ -38,11 +38,12 @@ export class TileMap extends React.PureComponent { padding: 4, isDraggable: true, layers: [], - renderMap: renderMap + renderMap: renderMap, } private oldState: State private canvas: HTMLCanvasElement | null + private strikeCanvas: HTMLCanvasElement private mounted: boolean private hover: Coord | null private popupTimeout: number | null @@ -61,17 +62,18 @@ export class TileMap extends React.PureComponent { pan: { x: panX, y: panY }, center: { x: x == null ? initialX : x, - y: y == null ? initialY : y + y: y == null ? initialY : y, }, size: zoom * size, zoom, - popup: null + popup: null, } this.state = this.generateState(props, initialState) this.oldState = this.state this.hover = null this.mounted = false this.canvas = null + this.strikeCanvas = document.createElement('canvas') this.popupTimeout = null } @@ -86,12 +88,12 @@ export class TileMap extends React.PureComponent { ...nextState, center: { x: nextProps.x, - y: nextProps.y + y: nextProps.y, }, pan: { x: 0, - y: 0 - } + y: 0, + }, } } @@ -122,7 +124,7 @@ export class TileMap extends React.PureComponent { if (newZoom !== this.props.zoom && newZoom !== this.state.zoom) { this.setState({ zoom: newZoom, - size: this.props.size * newZoom + size: this.props.size * newZoom, }) } } @@ -174,7 +176,7 @@ export class TileMap extends React.PureComponent { center, pan, size, - padding + padding, }) return { ...viewport, pan, zoom, center, size } } @@ -208,18 +210,18 @@ export class TileMap extends React.PureComponent { const boundaries = { nw: { x: minX - halfWidth, y: maxY + halfHeight }, - se: { x: maxX + halfWidth, y: minY - halfHeight } + se: { x: maxX + halfWidth, y: minY - halfHeight }, } const viewport = { nw: { x: this.state.center.x - halfWidth, - y: this.state.center.y + halfHeight + y: this.state.center.y + halfHeight, }, se: { x: this.state.center.x + halfWidth, - y: this.state.center.y - halfHeight - } + y: this.state.center.y - halfHeight, + }, } if (viewport.nw.x + newPan.x / newSize < boundaries.nw.x) { @@ -238,7 +240,7 @@ export class TileMap extends React.PureComponent { this.setState({ pan: newPan, zoom: newZoom, - size: newSize + size: newSize, }) this.renderMap() this.debouncedUpdateCenter() @@ -252,7 +254,7 @@ export class TileMap extends React.PureComponent { const viewportOffset = { x: (width - padding - 0.5) / 2 - center.x, - y: (height - padding) / 2 + center.y + y: (height - padding) / 2 + center.y, } const coordX = Math.round(panOffset.x - viewportOffset.x) @@ -326,7 +328,7 @@ export class TileMap extends React.PureComponent { if (this.mounted) { this.setState( { - popup: { x, y, top, left, visible: true } + popup: { x, y, top, left, visible: true }, }, () => onPopup(this.state.popup!) ) @@ -352,8 +354,8 @@ export class TileMap extends React.PureComponent { { popup: { ...this.state.popup, - visible: false - } + visible: false, + }, }, () => { onPopup(this.state.popup!) @@ -371,26 +373,31 @@ export class TileMap extends React.PureComponent { const newPan = { x: panX, y: panY } const newCenter = { x: center.x + Math.floor((pan.x - panX) / size), - y: center.y - Math.floor((pan.y - panY) / size) + y: center.y - Math.floor((pan.y - panY) / size), } this.setState({ pan: newPan, - center: newCenter + center: newCenter, }) } renderMap() { - if (!this.canvas) { + const strikeContext = this.strikeCanvas.getContext('2d') + + if (!this.canvas || !strikeContext) { return } const { width, height, layers, renderMap } = this.props const { nw, se, pan, size, center } = this.state + this.strikeCanvas.width = width + this.strikeCanvas.height = height const ctx = this.canvas.getContext('2d')! renderMap({ ctx, + strikeCanvasCtx: strikeContext, width, height, size, @@ -398,7 +405,7 @@ export class TileMap extends React.PureComponent { nw, se, center, - layers + layers, }) } diff --git a/src/components/TileMap/TileMap.types.ts b/src/components/TileMap/TileMap.types.ts index 6a12248..e204676 100644 --- a/src/components/TileMap/TileMap.types.ts +++ b/src/components/TileMap/TileMap.types.ts @@ -74,6 +74,7 @@ export type Popup = { export type MapRenderer = (args: { ctx: CanvasRenderingContext2D + strikeCanvasCtx: CanvasRenderingContext2D width: number height: number size: number diff --git a/src/render/map.ts b/src/render/map.ts index 3b77776..ebd7889 100644 --- a/src/render/map.ts +++ b/src/render/map.ts @@ -1,8 +1,15 @@ -import { renderTile } from './tile' +import { + addClippingContextForTileStrike, + buildStrikethroughPattern, + finishClipping, + initializeContextForTileStrike, + renderTile, +} from './tile' import { Coord, Layer } from '../lib/common' export function renderMap(args: { ctx: CanvasRenderingContext2D + strikeCanvasCtx: CanvasRenderingContext2D width: number height: number size: number @@ -12,14 +19,17 @@ export function renderMap(args: { center: Coord layers: Layer[] }) { - const { ctx, width, height, size, pan, nw, se, center, layers } = args - + const { ctx, strikeCanvasCtx, width, height, size, pan, nw, se, center, layers } = args + const strikethroughPattern = buildStrikethroughPattern(ctx, 1) + let clippingRegion: Path2D ctx.clearRect(0, 0, width, height) const halfWidth = width / 2 const halfHeight = height / 2 for (const layer of layers) { + clippingRegion = initializeContextForTileStrike(strikeCanvasCtx, width, height) + let hasStrikeTile = false for (let x = nw.x; x < se.x; x++) { for (let y = se.y; y < nw.y; y++) { const offsetX = (center.x - x) * size + (pan ? pan.x : 0) @@ -45,9 +55,36 @@ export function renderMap(args: { top, topLeft, scale, - strikethrough }) + + if (strikethrough) { + hasStrikeTile = true + addClippingContextForTileStrike({ + ctx: strikeCanvasCtx, + region: clippingRegion, + x: halfWidth - offsetX + halfSize, + y: halfHeight - offsetY + halfSize, + size, + padding: size < 7 ? 0.5 : size < 12 ? 1 : size < 18 ? 1.5 : 2, + offset: 1, + left, + top, + topLeft, + scale, + }) + } } } + if (strikethroughPattern && hasStrikeTile) { + finishClipping(strikeCanvasCtx, clippingRegion, strikethroughPattern, width, height) + } + // Draw the strike pattern + if ( + strikeCanvasCtx.canvas.width > 0 && + strikeCanvasCtx.canvas.height > 0 && + hasStrikeTile + ) { + ctx.drawImage(strikeCanvasCtx.canvas, 0, 0) + } } } diff --git a/src/render/tile.ts b/src/render/tile.ts index 5b96936..2634a90 100644 --- a/src/render/tile.ts +++ b/src/render/tile.ts @@ -1,65 +1,33 @@ -function strikethroughTile( +export function buildStrikethroughPattern( ctx: CanvasRenderingContext2D, - x: number, - y: number, - size: number, - position?: { - top?: boolean, - left?: boolean, - topLeft?: boolean + scale: number +): CanvasPattern | null { + const patternCanvas = document.createElement('canvas') + const patternContext = patternCanvas.getContext('2d') + if (!patternContext) { + return null } -) { - ctx.save() - ctx.beginPath() - ctx.lineWidth = 1 - ctx.lineCap = 'square' - // This is to make it centered, but I'm not that we need it really - // We need to make sure that this works correctly. - let clipX = x - let clipY = y - let clipSize = size - if (!position?.top && !position?.left) { - clipX += 1 - clipY += 1 - clipSize -= 2 - } else if (position?.top && position?.left && position?.topLeft) { - clipSize -= + 2 - } else { - if (position?.left) { - clipY -= 4 - } else if (position?.top) { - } - } - // Clipping rectangle - // ctx.fillRect(x, y, x + size, y + size) - ctx.moveTo(clipX, clipY) - ctx.lineTo(clipX, clipY + clipSize) - ctx.lineTo(clipX + clipSize, clipY + clipSize) - ctx.lineTo(clipX + clipSize, clipY) - ctx.lineTo(clipX, clipY) - ctx.closePath() - ctx.clip() + patternCanvas.width = 100 + patternCanvas.height = 100 - const color1 = '#000000' - const numberOfStripes = 6 - const thickness = size / numberOfStripes - ctx.globalAlpha = 0.3 + const blackColor = '#000000' + // Uses a fixed number of stripes with a thickness based on the original size of the tile. + const thickness = 10 * scale + const numberOfStripes = patternCanvas.width / thickness + patternContext.globalAlpha = 0.3 - for (var i = 0; i < numberOfStripes * 2; i++) { - if (i % 2 !== 0) { - continue - } - ctx.beginPath() - ctx.strokeStyle = color1 - ctx.lineWidth = thickness / 1.5 - ctx.lineCap = 'square' - ctx.moveTo(x + i * thickness + thickness / 2 - size, y) - ctx.lineTo(x + i * thickness + thickness / 2, y + size) - ctx.stroke() + for (var i = 0; i < numberOfStripes * 2; i += 2) { + patternContext.beginPath() + patternContext.strokeStyle = blackColor + patternContext.lineWidth = thickness / 1.5 + patternContext.lineCap = 'square' + patternContext.moveTo(i * thickness + thickness / 2 - patternCanvas.width, 0) + patternContext.lineTo(i * thickness + thickness / 2, patternCanvas.width) + patternContext.stroke() } - ctx.restore() + return ctx.createPattern(patternCanvas, 'repeat') } export function renderTile(args: { @@ -75,21 +43,9 @@ export function renderTile(args: { topLeft?: boolean scale?: number strikethrough?: boolean + strikethroughPattern?: CanvasPattern }) { - const { - ctx, - x, - y, - size, - padding, - offset, - color, - left, - top, - topLeft, - scale, - strikethrough - } = args + const { ctx, x, y, size, padding, offset, color, left, top, topLeft, scale } = args ctx.fillStyle = color @@ -103,20 +59,6 @@ export function renderTile(args: { tileSize - padding, tileSize - padding ) - - if (strikethrough) { - strikethroughTile( - ctx, - x - tileSize + padding, - y - tileSize + padding, - tileSize - padding, - { - top, - left, - topLeft - } - ) - } } else if (top && left && topLeft) { // connected everywhere: it's a square ctx.fillRect( @@ -125,20 +67,6 @@ export function renderTile(args: { tileSize + offset, tileSize + offset ) - - if (strikethrough) { - strikethroughTile( - ctx, - x - tileSize - offset, - y - tileSize - offset, - tileSize + offset, - { - top, - left, - topLeft - } - ) - } } else { if (left) { // connected left: it's a rectangle @@ -148,19 +76,6 @@ export function renderTile(args: { tileSize + offset, tileSize - padding ) - if (strikethrough) { - strikethroughTile( - ctx, - x - tileSize - offset, - y - tileSize + padding, - tileSize + offset, - { - top, - left, - topLeft - } - ) - } } if (top) { // connected top: it's a rectangle @@ -170,19 +85,80 @@ export function renderTile(args: { tileSize - padding, tileSize + offset ) - if (strikethrough) { - strikethroughTile( - ctx, - x - tileSize + padding, - y - tileSize - offset, - tileSize - padding, - { - top, - left, - topLeft - } - ) - } } } } + +export function initializeContextForTileStrike( + ctx: CanvasRenderingContext2D, + width: number, + height: number +): Path2D { + ctx.clearRect(0, 0, width, height) + ctx.beginPath() + ctx.save() + return new Path2D() +} + +export function addClippingContextForTileStrike(args: { + ctx: CanvasRenderingContext2D + region: Path2D + x: number + y: number + size: number + padding: number + offset: number + left?: boolean + top?: boolean + topLeft?: boolean + scale?: number +}): void { + const { x, y, size, padding, offset, left, top, topLeft, scale, region } = args + const tileSize = scale ? size * scale : size + + if (!top && !left) { + region.rect( + x - tileSize + padding, + y - tileSize + padding, + tileSize - padding, + tileSize - padding + ) + } else if (top && left && topLeft) { + region.rect( + x - tileSize - offset, + y - tileSize - offset, + tileSize + offset, + tileSize + offset + ) + } else { + if (left) { + region.rect( + x - tileSize - offset, + y - tileSize + padding, + tileSize + offset, + tileSize - padding + ) + } + if (top) { + region.rect( + x - tileSize + padding, + y - tileSize - offset, + tileSize - padding, + tileSize + offset + ) + } + } +} + +export function finishClipping( + ctx: CanvasRenderingContext2D, + region: Path2D, + pattern: CanvasPattern, + width: number, + height: number +) { + ctx.clip(region) + ctx.fillStyle = pattern + ctx.fillRect(0, 0, width, height) + ctx.restore() +} diff --git a/stories/10_Atlas_strikethrough_tiles.stories.tsx b/stories/10_Atlas_strikethrough_tiles.stories.tsx index 80312b7..2a9cf3e 100644 --- a/stories/10_Atlas_strikethrough_tiles.stories.tsx +++ b/stories/10_Atlas_strikethrough_tiles.stories.tsx @@ -5,83 +5,115 @@ import { storiesOf } from '@storybook/react' import { withKnobs } from '@storybook/addon-knobs' import { TileMap, Layer } from '../src' -import { Tile } from '../src/lib/common' +import { Coord, Tile } from '../src/lib/common' import './stories.css' +let selected: Coord[] = [] + +const isSelected = (x: number, y: number) => + selected.some((coord) => coord.x === x && coord.y === y) + +const handleClick = (x: number, y: number) => { + if (isSelected(x, y)) { + selected = selected.filter((coord) => !(coord.x === x && coord.y === y)) + } else { + selected.push({ x, y }) + } +} + +const selectedStrokeLayer: Layer = (x, y) => { + return isSelected(x, y) ? { color: '#ff0044', scale: 1.4 } : null +} + +const selectedFillLayer: Layer = (x, y) => { + const tile = layer.get(x)?.get(y) ? layer.get(x)!.get(y)! : undefined + + return isSelected(x, y) ? { color: '#ff9990', scale: 1.2, ...tile } : null +} + const layer = new Map>() -layer.set(0, new Map()) -layer.set(2, new Map()) -layer.set(3, new Map()) -layer.set(4, new Map()) -layer.set(5, new Map()) +for (let i = -5; i < 6; i++) { + layer.set(i, new Map()) +} // Single tile layer.get(0)?.set(0, { color: '#ff9990', - strikethrough: true + strikethrough: true, }) // Vertical rectangle -for(let i = 2; i < 5; i++) { +for (let i = 2; i < 5; i++) { layer.get(0)?.set(i, { color: '#ff9990', top: i !== 4, - strikethrough: true + strikethrough: true, }) } // Horizontal rectangle -for(let i = 2; i < 5; i++) { +for (let i = 2; i < 5; i++) { layer.get(i)?.set(0, { color: '#ff9990', left: i !== 2, - strikethrough: true + strikethrough: true, }) } // Square -for(let i = 2; i < 4; i++) { - for(let j = 2; j < 4; j++) { +for (let i = 2; i < 4; i++) { + for (let j = 2; j < 4; j++) { layer.get(i)?.set(j, { color: '#ff9990', left: i === 3, top: j === 2, - strikethrough: true + topLeft: i === 3 && j === 2, + strikethrough: true, }) } } +// Long rectangle +for (let i = -5; i < 0; i++) { + layer.get(i)?.set(-3, { + color: '#ff9990', + left: i !== -5, + strikethrough: true, + }) + layer.get(i)?.set(-4, { + color: '#ff9990', + left: i !== -5, + top: true, + topLeft: i !== -5, + strikethrough: true, + }) +} + // Tetris brick layer.get(2)?.set(-2, { color: '#ff9990', - strikethrough: true + strikethrough: true, }) layer.get(2)?.set(-3, { color: '#ff9990', - topLeft: true, top: true, - strikethrough: true + strikethrough: true, }) layer.get(3)?.set(-3, { color: '#ff9990', + topLeft: true, left: true, - strikethrough: true + strikethrough: true, }) layer.get(3)?.set(-4, { color: '#ff9990', top: true, - strikethrough: true + strikethrough: true, }) -const chessboardLayer: Layer = (x, y) => { - if (!layer.get(x)?.get(y)) { - return { - color: '#888888' - } - } - - return layer.get(x)!.get(y)! +const chessboardLayer: Layer = (x, y): Tile => { + return !layer.get(x)?.get(y) ? { color: '#888888' } : layer.get(x)!.get(y)! } const stories = storiesOf('TileMap', module) @@ -89,5 +121,10 @@ const stories = storiesOf('TileMap', module) stories.addDecorator(withKnobs) stories.add('10. Strikethrough layer', () => { - return + return ( + + ) }) From f4d0dfabad03acd144e60cdcbf7749dbcadb3dd6 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Fri, 30 Dec 2022 12:42:11 -0300 Subject: [PATCH 3/4] fix: Write JSDocs --- src/components/TileMap/TileMap.tsx | 3 +- src/components/TileMap/TileMap.types.ts | 1 + src/render/map.ts | 17 ++++- src/render/tile.ts | 96 ++++++++++++++++--------- 4 files changed, 80 insertions(+), 37 deletions(-) diff --git a/src/components/TileMap/TileMap.tsx b/src/components/TileMap/TileMap.tsx index f8d406c..2540554 100644 --- a/src/components/TileMap/TileMap.tsx +++ b/src/components/TileMap/TileMap.tsx @@ -390,7 +390,7 @@ export class TileMap extends React.PureComponent { } const { width, height, layers, renderMap } = this.props - const { nw, se, pan, size, center } = this.state + const { nw, se, pan, size, center, zoom } = this.state this.strikeCanvas.width = width this.strikeCanvas.height = height const ctx = this.canvas.getContext('2d')! @@ -406,6 +406,7 @@ export class TileMap extends React.PureComponent { se, center, layers, + zoom, }) } diff --git a/src/components/TileMap/TileMap.types.ts b/src/components/TileMap/TileMap.types.ts index e204676..9a65a47 100644 --- a/src/components/TileMap/TileMap.types.ts +++ b/src/components/TileMap/TileMap.types.ts @@ -83,4 +83,5 @@ export type MapRenderer = (args: { se: Coord center: Coord layers: Layer[] + zoom: number }) => void diff --git a/src/render/map.ts b/src/render/map.ts index ebd7889..8b098fa 100644 --- a/src/render/map.ts +++ b/src/render/map.ts @@ -1,7 +1,7 @@ import { addClippingContextForTileStrike, buildStrikethroughPattern, - finishClipping, + finishClippingForTileStrike, initializeContextForTileStrike, renderTile, } from './tile' @@ -18,9 +18,13 @@ export function renderMap(args: { se: Coord center: Coord layers: Layer[] + zoom: number }) { const { ctx, strikeCanvasCtx, width, height, size, pan, nw, se, center, layers } = args + // Creates the strikethrough pattern that will be used to draw the strikethrough in the tiles. + // The scale is set to 1 as scaling the pattern makes it very difficult to be used into a repeatable pattern. const strikethroughPattern = buildStrikethroughPattern(ctx, 1) + // Sets up the clipping region to be used in the strike process. let clippingRegion: Path2D ctx.clearRect(0, 0, width, height) @@ -28,6 +32,7 @@ export function renderMap(args: { const halfHeight = height / 2 for (const layer of layers) { + // Initializes the strike canvas and its context clippingRegion = initializeContextForTileStrike(strikeCanvasCtx, width, height) let hasStrikeTile = false for (let x = nw.x; x < se.x; x++) { @@ -59,6 +64,7 @@ export function renderMap(args: { if (strikethrough) { hasStrikeTile = true + // Adds the rectangles figures to the clipping region to later clip everything outside of those regions addClippingContextForTileStrike({ ctx: strikeCanvasCtx, region: clippingRegion, @@ -76,7 +82,14 @@ export function renderMap(args: { } } if (strikethroughPattern && hasStrikeTile) { - finishClipping(strikeCanvasCtx, clippingRegion, strikethroughPattern, width, height) + // Applies the pattern and clips the regions to produce the strike pattern over the tiles. + finishClippingForTileStrike( + strikeCanvasCtx, + clippingRegion, + strikethroughPattern, + width, + height + ) } // Draw the strike pattern if ( diff --git a/src/render/tile.ts b/src/render/tile.ts index 2634a90..fea3c42 100644 --- a/src/render/tile.ts +++ b/src/render/tile.ts @@ -1,35 +1,3 @@ -export function buildStrikethroughPattern( - ctx: CanvasRenderingContext2D, - scale: number -): CanvasPattern | null { - const patternCanvas = document.createElement('canvas') - const patternContext = patternCanvas.getContext('2d') - if (!patternContext) { - return null - } - - patternCanvas.width = 100 - patternCanvas.height = 100 - - const blackColor = '#000000' - // Uses a fixed number of stripes with a thickness based on the original size of the tile. - const thickness = 10 * scale - const numberOfStripes = patternCanvas.width / thickness - patternContext.globalAlpha = 0.3 - - for (var i = 0; i < numberOfStripes * 2; i += 2) { - patternContext.beginPath() - patternContext.strokeStyle = blackColor - patternContext.lineWidth = thickness / 1.5 - patternContext.lineCap = 'square' - patternContext.moveTo(i * thickness + thickness / 2 - patternCanvas.width, 0) - patternContext.lineTo(i * thickness + thickness / 2, patternCanvas.width) - patternContext.stroke() - } - - return ctx.createPattern(patternCanvas, 'repeat') -} - export function renderTile(args: { ctx: CanvasRenderingContext2D x: number @@ -89,6 +57,53 @@ export function renderTile(args: { } } +/** + * Creates a strikethrough pattern that can be later used to draw over a canvas. + * @param ctx The context of the + * @param scale The scaling factor for the lines thickness. + * @returns a CanvasPattern with the strikethrough pattern or null if the browser didn't provide a context. + */ +export function buildStrikethroughPattern( + ctx: CanvasRenderingContext2D, + scale?: number +): CanvasPattern | null { + const patternCanvas = document.createElement('canvas') + const patternContext = patternCanvas.getContext('2d') + if (!patternContext) { + return null + } + + patternCanvas.width = 200 + patternCanvas.height = 200 + + const blackColor = '#000000' + // Uses a fixed number of stripes with a thickness based on the original size of the tile. + const thickness = 10 * (scale ?? 1) + const numberOfStripes = patternCanvas.width / thickness + patternContext.globalAlpha = 0.3 + + for (var i = 0; i < numberOfStripes * 2; i += 2) { + patternContext.beginPath() + patternContext.strokeStyle = blackColor + patternContext.lineWidth = thickness / 1.5 + patternContext.lineCap = 'square' + patternContext.moveTo(i * thickness + thickness / 2 - patternCanvas.width, 0) + patternContext.lineTo(i * thickness + thickness / 2, patternCanvas.width) + patternContext.stroke() + } + + return ctx.createPattern(patternCanvas, 'repeat') +} + +/** + * Initializes the process of creating the strikethrough clipping canvas. + * This function clears the existing canvas, starts a new path where to write the clipping rectangles + * that will be used to draw the tiles and creates the path for the clipping rectangles. + * @param ctx The canvas context where to draw the strikethrough screen. + * @param width The width of the canvas. + * @param height The height of the canvas. + * @returns a new Path2D where all the clipping rectangles will be added to. + */ export function initializeContextForTileStrike( ctx: CanvasRenderingContext2D, width: number, @@ -96,10 +111,13 @@ export function initializeContextForTileStrike( ): Path2D { ctx.clearRect(0, 0, width, height) ctx.beginPath() - ctx.save() return new Path2D() } +/** + * Draws a clipping rectangle and adds it to the clipping region to be later clipped. + * @param args The set of arguments that define how to draw a clipping rectangle. + */ export function addClippingContextForTileStrike(args: { ctx: CanvasRenderingContext2D region: Path2D @@ -150,13 +168,23 @@ export function addClippingContextForTileStrike(args: { } } -export function finishClipping( +/** + * Fills the canvas with a pattern and clips the figures defined in the region to produce a canvas that + * contains figures with a strikethrough pattern. + * @param ctx The context of the canvas where to draw the figures. + * @param region The region containing the figures to be clipped out of the pattern. + * @param pattern The pattern which will server as the figures background after clipping them. + * @param width The width of the canvas to write the strike pattern onto. + * @param height The height of the canvas to write the strike pattern onto. + */ +export function finishClippingForTileStrike( ctx: CanvasRenderingContext2D, region: Path2D, pattern: CanvasPattern, width: number, height: number ) { + ctx.save() ctx.clip(region) ctx.fillStyle = pattern ctx.fillRect(0, 0, width, height) From a23169a819fd246460c609d76f3107f79ae857ee Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Fri, 30 Dec 2022 14:16:14 -0300 Subject: [PATCH 4/4] fix: Improve performance by rendering the lines in the pattern once --- src/render/tile.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/render/tile.ts b/src/render/tile.ts index fea3c42..5e9e59f 100644 --- a/src/render/tile.ts +++ b/src/render/tile.ts @@ -82,15 +82,15 @@ export function buildStrikethroughPattern( const numberOfStripes = patternCanvas.width / thickness patternContext.globalAlpha = 0.3 + patternContext.beginPath() for (var i = 0; i < numberOfStripes * 2; i += 2) { - patternContext.beginPath() patternContext.strokeStyle = blackColor patternContext.lineWidth = thickness / 1.5 patternContext.lineCap = 'square' patternContext.moveTo(i * thickness + thickness / 2 - patternCanvas.width, 0) patternContext.lineTo(i * thickness + thickness / 2, patternCanvas.width) - patternContext.stroke() } + patternContext.stroke() return ctx.createPattern(patternCanvas, 'repeat') }