Merge pull request #11 from dscyrescotti/canvas

Implement canvas rendering
This commit is contained in:
Aye Chan
2024-05-04 15:02:42 +08:00
committed by GitHub
54 changed files with 3035 additions and 1 deletions

View File

@@ -12,6 +12,56 @@
EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; };
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738792BE5EF0400A4542E /* MemosView.swift */; };
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7387C2BE5EF4B00A4542E /* MemoView.swift */; };
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738822BE5FEFE00A4542E /* RenderPass.swift */; };
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738852BE5FF2500A4542E /* Canvas.swift */; };
ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738872BE5FF4400A4542E /* Renderer.swift */; };
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738892BE6006A00A4542E /* PipelineStates.swift */; };
ECA7388C2BE6009600A4542E /* Textures.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7388B2BE6009600A4542E /* Textures.swift */; };
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA7388E2BE600DA00A4542E /* Grid.metal */; };
ECA738912BE600F500A4542E /* Cache.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA738902BE600F500A4542E /* Cache.metal */; };
ECA738932BE6011100A4542E /* Stroke.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA738922BE6011100A4542E /* Stroke.metal */; };
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA738942BE6012D00A4542E /* ViewPort.metal */; };
ECA738972BE6014200A4542E /* Graphic.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA738962BE6014200A4542E /* Graphic.metal */; };
ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7389B2BE601AF00A4542E /* GridVertex.swift */; };
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7389D2BE601CB00A4542E /* QuadVertex.swift */; };
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */; };
ECA738A32BE6020A00A4542E /* CGFloat++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A22BE6020A00A4542E /* CGFloat++.swift */; };
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A52BE6023F00A4542E /* GridUniforms.swift */; };
ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A72BE6025900A4542E /* GraphicUniforms.swift */; };
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A92BE6026D00A4542E /* Uniforms.swift */; };
ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738AC2BE60CC600A4542E /* DrawingView.swift */; };
ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738AF2BE60D0B00A4542E /* CanvasViewController.swift */; };
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738B22BE60D9E00A4542E /* CanvasView.swift */; };
ECA738B62BE60DCD00A4542E /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738B52BE60DCD00A4542E /* History.swift */; };
ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738B72BE60DDC00A4542E /* HistoryEvent.swift */; };
ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738B92BE60DEF00A4542E /* HistoryAction.swift */; };
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738BB2BE60E0300A4542E /* Tool.swift */; };
ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738BE2BE60E3400A4542E /* Pen.swift */; };
ECA738C12BE60E5300A4542E /* PenStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738C02BE60E5300A4542E /* PenStyle.swift */; };
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738C32BE60E8800A4542E /* MarkerPenStyle.swift */; };
ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738C52BE60E9D00A4542E /* EraserPenStyle.swift */; };
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738C82BE60EF700A4542E /* GraphicContext.swift */; };
ECA738CB2BE60F1900A4542E /* ViewPortContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738CA2BE60F1900A4542E /* ViewPortContext.swift */; };
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738CC2BE60F2F00A4542E /* GridContext.swift */; };
ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738D12BE60F7B00A4542E /* Stroke.swift */; };
ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */; };
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738D62BE60FC100A4542E /* SolidPointStrokeGenerator.swift */; };
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738D92BE60FF100A4542E /* CacheRenderPass.swift */; };
ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738DB2BE6108D00A4542E /* StrokeRenderPass.swift */; };
ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */; };
ECA738E02BE610B900A4542E /* EraserRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */; };
ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */; };
ECA738E42BE6110800A4542E /* Drawable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E32BE6110800A4542E /* Drawable.swift */; };
ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E52BE611FD00A4542E /* CGRect++.swift */; };
ECA738E82BE6120F00A4542E /* Color++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E72BE6120F00A4542E /* Color++.swift */; };
ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E92BE6122E00A4542E /* CGPoint++.swift */; };
ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738EB2BE6124E00A4542E /* CGAffineTransform++.swift */; };
ECA738EE2BE6125D00A4542E /* simd_float4x4++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */; };
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738EF2BE6127700A4542E /* CGSize++.swift */; };
ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F12BE6128F00A4542E /* Collection++.swift */; };
ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; };
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
ECA738F82BE612EB00A4542E /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F72BE612EB00A4542E /* Quad.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -21,6 +71,56 @@
EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
ECA738792BE5EF0400A4542E /* MemosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemosView.swift; sourceTree = "<group>"; };
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoView.swift; sourceTree = "<group>"; };
ECA738822BE5FEFE00A4542E /* RenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderPass.swift; sourceTree = "<group>"; };
ECA738852BE5FF2500A4542E /* Canvas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Canvas.swift; sourceTree = "<group>"; };
ECA738872BE5FF4400A4542E /* Renderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Renderer.swift; sourceTree = "<group>"; };
ECA738892BE6006A00A4542E /* PipelineStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PipelineStates.swift; sourceTree = "<group>"; };
ECA7388B2BE6009600A4542E /* Textures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Textures.swift; sourceTree = "<group>"; };
ECA7388E2BE600DA00A4542E /* Grid.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Grid.metal; sourceTree = "<group>"; };
ECA738902BE600F500A4542E /* Cache.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Cache.metal; sourceTree = "<group>"; };
ECA738922BE6011100A4542E /* Stroke.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Stroke.metal; sourceTree = "<group>"; };
ECA738942BE6012D00A4542E /* ViewPort.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = ViewPort.metal; sourceTree = "<group>"; };
ECA738962BE6014200A4542E /* Graphic.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Graphic.metal; sourceTree = "<group>"; };
ECA7389B2BE601AF00A4542E /* GridVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridVertex.swift; sourceTree = "<group>"; };
ECA7389D2BE601CB00A4542E /* QuadVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadVertex.swift; sourceTree = "<group>"; };
ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPortVertex.swift; sourceTree = "<group>"; };
ECA738A22BE6020A00A4542E /* CGFloat++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat++.swift"; sourceTree = "<group>"; };
ECA738A52BE6023F00A4542E /* GridUniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridUniforms.swift; sourceTree = "<group>"; };
ECA738A72BE6025900A4542E /* GraphicUniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicUniforms.swift; sourceTree = "<group>"; };
ECA738A92BE6026D00A4542E /* Uniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uniforms.swift; sourceTree = "<group>"; };
ECA738AC2BE60CC600A4542E /* DrawingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawingView.swift; sourceTree = "<group>"; };
ECA738AF2BE60D0B00A4542E /* CanvasViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasViewController.swift; sourceTree = "<group>"; };
ECA738B22BE60D9E00A4542E /* CanvasView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasView.swift; sourceTree = "<group>"; };
ECA738B52BE60DCD00A4542E /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = "<group>"; };
ECA738B72BE60DDC00A4542E /* HistoryEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryEvent.swift; sourceTree = "<group>"; };
ECA738B92BE60DEF00A4542E /* HistoryAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryAction.swift; sourceTree = "<group>"; };
ECA738BB2BE60E0300A4542E /* Tool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tool.swift; sourceTree = "<group>"; };
ECA738BE2BE60E3400A4542E /* Pen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pen.swift; sourceTree = "<group>"; };
ECA738C02BE60E5300A4542E /* PenStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenStyle.swift; sourceTree = "<group>"; };
ECA738C32BE60E8800A4542E /* MarkerPenStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkerPenStyle.swift; sourceTree = "<group>"; };
ECA738C52BE60E9D00A4542E /* EraserPenStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserPenStyle.swift; sourceTree = "<group>"; };
ECA738C82BE60EF700A4542E /* GraphicContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicContext.swift; sourceTree = "<group>"; };
ECA738CA2BE60F1900A4542E /* ViewPortContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPortContext.swift; sourceTree = "<group>"; };
ECA738CC2BE60F2F00A4542E /* GridContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridContext.swift; sourceTree = "<group>"; };
ECA738D12BE60F7B00A4542E /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = "<group>"; };
ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeGenerator.swift; sourceTree = "<group>"; };
ECA738D62BE60FC100A4542E /* SolidPointStrokeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidPointStrokeGenerator.swift; sourceTree = "<group>"; };
ECA738D92BE60FF100A4542E /* CacheRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheRenderPass.swift; sourceTree = "<group>"; };
ECA738DB2BE6108D00A4542E /* StrokeRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeRenderPass.swift; sourceTree = "<group>"; };
ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPortRenderPass.swift; sourceTree = "<group>"; };
ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserRenderPass.swift; sourceTree = "<group>"; };
ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicRenderPass.swift; sourceTree = "<group>"; };
ECA738E32BE6110800A4542E /* Drawable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drawable.swift; sourceTree = "<group>"; };
ECA738E52BE611FD00A4542E /* CGRect++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect++.swift"; sourceTree = "<group>"; };
ECA738E72BE6120F00A4542E /* Color++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color++.swift"; sourceTree = "<group>"; };
ECA738E92BE6122E00A4542E /* CGPoint++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint++.swift"; sourceTree = "<group>"; };
ECA738EB2BE6124E00A4542E /* CGAffineTransform++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGAffineTransform++.swift"; sourceTree = "<group>"; };
ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "simd_float4x4++.swift"; sourceTree = "<group>"; };
ECA738EF2BE6127700A4542E /* CGSize++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGSize++.swift"; sourceTree = "<group>"; };
ECA738F12BE6128F00A4542E /* Collection++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection++.swift"; sourceTree = "<group>"; };
ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = "<group>"; };
ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<group>"; };
ECA738F72BE612EB00A4542E /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -54,9 +154,11 @@
isa = PBXGroup;
children = (
ECA738762BE5EE4E00A4542E /* App */,
ECA7387E2BE5FE4200A4542E /* Canvas */,
ECA738A12BE601F700A4542E /* Extensions */,
ECA738772BE5EEE800A4542E /* Features */,
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */,
EC7F6BF12BE5E6E400A34A7B /* Preview Content */,
ECA738802BE5FE6000A4542E /* Resources */,
);
path = Memola;
sourceTree = "<group>";
@@ -102,6 +204,222 @@
path = Memo;
sourceTree = "<group>";
};
ECA7387E2BE5FE4200A4542E /* Canvas */ = {
isa = PBXGroup;
children = (
ECA738F92BE6130000A4542E /* Geometries */,
ECA738812BE5FEEE00A4542E /* Abstracts */,
ECA738992BE6018900A4542E /* Buffers */,
ECA738C72BE60EE200A4542E /* Contexts */,
ECA738842BE5FF1B00A4542E /* Core */,
ECA738B42BE60DC200A4542E /* History */,
ECA738D82BE60FE200A4542E /* RenderPasses */,
ECA7388D2BE600BB00A4542E /* Shaders */,
ECA738B12BE60D8800A4542E /* Tool */,
ECA738AB2BE60CB500A4542E /* View */,
);
path = Canvas;
sourceTree = "<group>";
};
ECA738802BE5FE6000A4542E /* Resources */ = {
isa = PBXGroup;
children = (
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */,
);
path = Resources;
sourceTree = "<group>";
};
ECA738812BE5FEEE00A4542E /* Abstracts */ = {
isa = PBXGroup;
children = (
ECA738822BE5FEFE00A4542E /* RenderPass.swift */,
ECA738E32BE6110800A4542E /* Drawable.swift */,
);
path = Abstracts;
sourceTree = "<group>";
};
ECA738842BE5FF1B00A4542E /* Core */ = {
isa = PBXGroup;
children = (
ECA738852BE5FF2500A4542E /* Canvas.swift */,
ECA738892BE6006A00A4542E /* PipelineStates.swift */,
ECA738872BE5FF4400A4542E /* Renderer.swift */,
ECA7388B2BE6009600A4542E /* Textures.swift */,
);
path = Core;
sourceTree = "<group>";
};
ECA7388D2BE600BB00A4542E /* Shaders */ = {
isa = PBXGroup;
children = (
ECA7388E2BE600DA00A4542E /* Grid.metal */,
ECA738902BE600F500A4542E /* Cache.metal */,
ECA738922BE6011100A4542E /* Stroke.metal */,
ECA738942BE6012D00A4542E /* ViewPort.metal */,
ECA738962BE6014200A4542E /* Graphic.metal */,
);
path = Shaders;
sourceTree = "<group>";
};
ECA738982BE6015700A4542E /* Primitives */ = {
isa = PBXGroup;
children = (
ECA738F72BE612EB00A4542E /* Quad.swift */,
);
path = Primitives;
sourceTree = "<group>";
};
ECA738992BE6018900A4542E /* Buffers */ = {
isa = PBXGroup;
children = (
ECA738A42BE6022F00A4542E /* Uniforms */,
ECA7389A2BE6019700A4542E /* Vertices */,
);
path = Buffers;
sourceTree = "<group>";
};
ECA7389A2BE6019700A4542E /* Vertices */ = {
isa = PBXGroup;
children = (
ECA7389B2BE601AF00A4542E /* GridVertex.swift */,
ECA7389D2BE601CB00A4542E /* QuadVertex.swift */,
ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */,
);
path = Vertices;
sourceTree = "<group>";
};
ECA738A12BE601F700A4542E /* Extensions */ = {
isa = PBXGroup;
children = (
ECA738F32BE612A000A4542E /* Array++.swift */,
ECA738EB2BE6124E00A4542E /* CGAffineTransform++.swift */,
ECA738A22BE6020A00A4542E /* CGFloat++.swift */,
ECA738E92BE6122E00A4542E /* CGPoint++.swift */,
ECA738E52BE611FD00A4542E /* CGRect++.swift */,
ECA738EF2BE6127700A4542E /* CGSize++.swift */,
ECA738F12BE6128F00A4542E /* Collection++.swift */,
ECA738E72BE6120F00A4542E /* Color++.swift */,
ECA738F52BE612B700A4542E /* MTLDevice++.swift */,
ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
ECA738A42BE6022F00A4542E /* Uniforms */ = {
isa = PBXGroup;
children = (
ECA738A52BE6023F00A4542E /* GridUniforms.swift */,
ECA738A72BE6025900A4542E /* GraphicUniforms.swift */,
ECA738A92BE6026D00A4542E /* Uniforms.swift */,
);
path = Uniforms;
sourceTree = "<group>";
};
ECA738AB2BE60CB500A4542E /* View */ = {
isa = PBXGroup;
children = (
ECA738AE2BE60CEC00A4542E /* Bridge */,
ECA738B22BE60D9E00A4542E /* CanvasView.swift */,
);
path = View;
sourceTree = "<group>";
};
ECA738AE2BE60CEC00A4542E /* Bridge */ = {
isa = PBXGroup;
children = (
ECA738AC2BE60CC600A4542E /* DrawingView.swift */,
ECA738AF2BE60D0B00A4542E /* CanvasViewController.swift */,
);
path = Bridge;
sourceTree = "<group>";
};
ECA738B12BE60D8800A4542E /* Tool */ = {
isa = PBXGroup;
children = (
ECA738C22BE60E7200A4542E /* PenStyles */,
ECA738BD2BE60E2800A4542E /* Pen */,
ECA738BB2BE60E0300A4542E /* Tool.swift */,
);
path = Tool;
sourceTree = "<group>";
};
ECA738B42BE60DC200A4542E /* History */ = {
isa = PBXGroup;
children = (
ECA738B52BE60DCD00A4542E /* History.swift */,
ECA738B72BE60DDC00A4542E /* HistoryEvent.swift */,
ECA738B92BE60DEF00A4542E /* HistoryAction.swift */,
);
path = History;
sourceTree = "<group>";
};
ECA738BD2BE60E2800A4542E /* Pen */ = {
isa = PBXGroup;
children = (
ECA738BE2BE60E3400A4542E /* Pen.swift */,
ECA738C02BE60E5300A4542E /* PenStyle.swift */,
);
path = Pen;
sourceTree = "<group>";
};
ECA738C22BE60E7200A4542E /* PenStyles */ = {
isa = PBXGroup;
children = (
ECA738C32BE60E8800A4542E /* MarkerPenStyle.swift */,
ECA738C52BE60E9D00A4542E /* EraserPenStyle.swift */,
);
path = PenStyles;
sourceTree = "<group>";
};
ECA738C72BE60EE200A4542E /* Contexts */ = {
isa = PBXGroup;
children = (
ECA738C82BE60EF700A4542E /* GraphicContext.swift */,
ECA738CA2BE60F1900A4542E /* ViewPortContext.swift */,
ECA738CC2BE60F2F00A4542E /* GridContext.swift */,
);
path = Contexts;
sourceTree = "<group>";
};
ECA738CE2BE60F5000A4542E /* Stroke */ = {
isa = PBXGroup;
children = (
ECA738D52BE60FA200A4542E /* Generators */,
ECA738D12BE60F7B00A4542E /* Stroke.swift */,
ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */,
);
path = Stroke;
sourceTree = "<group>";
};
ECA738D52BE60FA200A4542E /* Generators */ = {
isa = PBXGroup;
children = (
ECA738D62BE60FC100A4542E /* SolidPointStrokeGenerator.swift */,
);
path = Generators;
sourceTree = "<group>";
};
ECA738D82BE60FE200A4542E /* RenderPasses */ = {
isa = PBXGroup;
children = (
ECA738D92BE60FF100A4542E /* CacheRenderPass.swift */,
ECA738DB2BE6108D00A4542E /* StrokeRenderPass.swift */,
ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */,
ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */,
ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */,
);
path = RenderPasses;
sourceTree = "<group>";
};
ECA738F92BE6130000A4542E /* Geometries */ = {
isa = PBXGroup;
children = (
ECA738CE2BE60F5000A4542E /* Stroke */,
ECA738982BE6015700A4542E /* Primitives */,
);
path = Geometries;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -172,9 +490,59 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */,
ECA738E42BE6110800A4542E /* Drawable.swift in Sources */,
ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */,
ECA738E02BE610B900A4542E /* EraserRenderPass.swift in Sources */,
ECA738912BE600F500A4542E /* Cache.metal in Sources */,
ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */,
ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */,
ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */,
ECA738E82BE6120F00A4542E /* Color++.swift in Sources */,
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */,
ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */,
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */,
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */,
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */,
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */,
ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */,
ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */,
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */,
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */,
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */,
ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */,
ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */,
ECA738CB2BE60F1900A4542E /* ViewPortContext.swift in Sources */,
ECA738EE2BE6125D00A4542E /* simd_float4x4++.swift in Sources */,
ECA7388C2BE6009600A4542E /* Textures.swift in Sources */,
ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */,
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */,
ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */,
ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */,
ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */,
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */,
ECA738932BE6011100A4542E /* Stroke.metal in Sources */,
ECA738B62BE60DCD00A4542E /* History.swift in Sources */,
ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */,
ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */,
ECA738A32BE6020A00A4542E /* CGFloat++.swift in Sources */,
ECA738C12BE60E5300A4542E /* PenStyle.swift in Sources */,
ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */,
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
ECA738F82BE612EB00A4542E /* Quad.swift in Sources */,
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -0,0 +1,14 @@
//
// Drawable.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
protocol Drawable: AnyObject {
func prepare(device: MTLDevice)
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder)
}

View File

@@ -0,0 +1,16 @@
//
// RenderPass.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
protocol RenderPass {
var label: String { get }
var descriptor: MTLRenderPassDescriptor? { get set }
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer)
func draw(on canvas: Canvas, with renderer: Renderer)
}

View File

@@ -0,0 +1,17 @@
//
// GraphicUniforms.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
struct GraphicUniforms {
var color: vector_float4
init(color: [CGFloat]) {
self.color = [color[0].float, color[1].float, color[2].float, color[3].float]
}
}

View File

@@ -0,0 +1,15 @@
//
// GridUniforms.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
struct GridUniforms {
var ratio: Float
var zoom: Float
var transform: simd_float4x4
}

View File

@@ -0,0 +1,13 @@
//
// Uniforms.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
struct Uniforms {
var transform: simd_float4x4
}

View File

@@ -0,0 +1,20 @@
//
// GridVertex.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
struct GridVertex {
var position: vector_float4
var pointSize: Float = 10
}
extension GridVertex {
init(x: CGFloat, y: CGFloat) {
self.position = [x.float, y.float, 0, 1]
}
}

View File

@@ -0,0 +1,32 @@
//
// QuadVertex.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
struct QuadVertex: Codable {
var position: vector_float4
var textCoord: vector_float2
var color: vector_float4
var origin: vector_float2
var rotation: Float
}
extension QuadVertex {
init(x: CGFloat, y: CGFloat, textCoord: CGPoint, color: [CGFloat], origin: CGPoint, rotation: CGFloat) {
self.position = [x.float, y.float, 0, 1]
self.textCoord = [textCoord.x.float, textCoord.y.float]
self.color = [color[0].float, color[1].float, color[2].float, color[3].float]
self.origin = [origin.x.float, origin.y.float]
self.rotation = rotation.float
}
func getOrigin() -> CGPoint {
CGPoint(x: CGFloat(origin[0]), y: CGFloat(origin[1]))
}
}

View File

@@ -0,0 +1,21 @@
//
// ViewPortVertex.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
struct ViewPortVertex {
var position: vector_float4
var textCoord: vector_float2
}
extension ViewPortVertex {
init(x: CGFloat, y: CGFloat, textCoord: CGPoint) {
self.position = [x.float, y.float, 0, 1]
self.textCoord = [textCoord.x.float, textCoord.y.float]
}
}

View File

@@ -0,0 +1,129 @@
//
// GraphicContext.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Combine
import MetalKit
import Foundation
protocol GraphicContextDelegate: AnyObject {
var didUpdate: PassthroughSubject<Void, Never> { get set }
}
class GraphicContext: Codable {
var strokes: [Stroke] = []
var currentStroke: Stroke?
var previousStroke: Stroke?
var currentPoint: CGPoint?
var renderType: RenderType = .finished
var vertices: [ViewPortVertex] = []
var vertexCount: Int = 4
var vertexBuffer: MTLBuffer?
weak var delegate: GraphicContextDelegate?
init() {
setViewPortVertices()
}
enum CodingKeys: CodingKey {
case strokes
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.strokes = try container.decode([Stroke].self, forKey: .strokes)
setViewPortVertices()
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.strokes, forKey: .strokes)
}
func setViewPortVertices() {
vertexBuffer = nil
vertices = [
ViewPortVertex(x: -1, y: -1, textCoord: CGPoint(x: 0, y: 1)),
ViewPortVertex(x: -1, y: 1, textCoord: CGPoint(x: 0, y: 0)),
ViewPortVertex(x: 1, y: -1, textCoord: CGPoint(x: 1, y: 1)),
ViewPortVertex(x: 1, y: 1, textCoord: CGPoint(x: 1, y: 0)),
]
}
func undoGraphic() {
guard !strokes.isEmpty else { return }
strokes.removeLast()
previousStroke = nil
delegate?.didUpdate.send()
}
func redoGraphic(for event: HistoryEvent) {
switch event {
case .stroke(let stroke):
strokes.append(stroke)
previousStroke = nil
}
delegate?.didUpdate.send()
}
}
extension GraphicContext: Drawable {
func prepare(device: MTLDevice) {
guard vertexBuffer == nil else {
return
}
vertexCount = vertices.count
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<ViewPortVertex>.stride, options: [])
}
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
prepare(device: device)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertices.count)
}
}
extension GraphicContext {
func beginStroke(at point: CGPoint, pen: Pen) -> Stroke {
let stroke = Stroke(
color: pen.color,
style: pen.style,
thickness: pen.thickness
)
strokes.append(stroke)
currentStroke = stroke
currentPoint = point
currentStroke?.begin(at: point)
return stroke
}
func appendStroke(with point: CGPoint) {
guard let currentStroke else { return }
guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.style.stepRate else { return }
currentStroke.append(to: point)
self.currentPoint = point
}
func endStroke(at point: CGPoint) {
guard currentPoint != nil else { return }
currentStroke?.finish(at: point)
previousStroke = currentStroke
currentStroke = nil
self.currentPoint = nil
delegate?.didUpdate.send()
}
}
extension GraphicContext {
enum RenderType {
case inProgress
case newlyFinished
case finished
}
}

View File

@@ -0,0 +1,45 @@
//
// GridContext.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
class GridContext {
var vertices: [GridVertex] = []
var vertexCount: Int = 0
var vertexBuffer: MTLBuffer?
init() {
generateVertices()
}
func generateVertices() {
let steps = stride(from: -10, through: 110, by: 0.25)
for y in steps {
for x in steps {
vertices.append(GridVertex(x: CGFloat(x), y: CGFloat(y)))
}
}
vertexCount = vertices.count
}
}
extension GridContext: Drawable {
func prepare(device: MTLDevice) {
guard vertexBuffer == nil else {
return
}
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<GridVertex>.stride, options: [])
}
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
guard vertexCount > .zero else { return }
prepare(device: device)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: vertexCount)
}
}

View File

@@ -0,0 +1,50 @@
//
// ViewPortContext.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
class ViewPortContext {
var vertices: [ViewPortVertex] = []
let vertexCount: Int = 4
var vertexBuffer: MTLBuffer?
func setViewPortVertices() {
vertexBuffer = nil
vertices = [
ViewPortVertex(x: -1, y: -1, textCoord: CGPoint(x: 0, y: 1)),
ViewPortVertex(x: -1, y: 1, textCoord: CGPoint(x: 0, y: 0)),
ViewPortVertex(x: 1, y: -1, textCoord: CGPoint(x: 1, y: 1)),
ViewPortVertex(x: 1, y: 1, textCoord: CGPoint(x: 1, y: 0)),
]
}
func setViewPortUpdateVertices(from bounds: CGRect) {
vertexBuffer = nil
vertices = [
ViewPortVertex(x: bounds.minX, y: bounds.minY, textCoord: CGPoint(x: 0, y: 0)),
ViewPortVertex(x: bounds.minX, y: bounds.maxY, textCoord: CGPoint(x: 0, y: 1)),
ViewPortVertex(x: bounds.maxX, y: bounds.minY, textCoord: CGPoint(x: 1, y: 0)),
ViewPortVertex(x: bounds.maxX, y: bounds.maxY, textCoord: CGPoint(x: 1, y: 1)),
]
}
}
extension ViewPortContext: Drawable {
func prepare(device: MTLDevice) {
guard vertexBuffer == nil else {
return
}
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<ViewPortVertex>.stride, options: [])
}
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
prepare(device: device)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertices.count)
}
}

View File

@@ -0,0 +1,191 @@
//
// Canvas.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Combine
import CoreData
import MetalKit
import Foundation
final class Canvas: NSObject, ObservableObject, Identifiable, Codable, GraphicContextDelegate {
let size: CGSize
let maximumZoomScale: CGFloat = 30
let minimumZoomScale: CGFloat = 3.1
var transform: simd_float4x4 = .init()
var uniformsBuffer: MTLBuffer?
let gridContext = GridContext()
var graphicContext = GraphicContext()
let viewPortContext = ViewPortContext()
var clipBounds: CGRect = .zero
var zoomScale: CGFloat = .zero
// weak var board: BoardObject?
var graphicLoader: (() throws -> GraphicContext)?
@Published var state: State = .initial
lazy var didUpdate = PassthroughSubject<Void, Never>()
init(size: CGSize = .init(width: 8_000, height: 8_000)) {
self.size = size
}
enum CodingKeys: CodingKey {
case size
case graphicContext
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.size = try container.decode(CGSize.self, forKey: .size)
self.graphicLoader = { try container.decode(GraphicContext.self, forKey: .graphicContext) }
}
}
// MARK: - Actions
extension Canvas {
func load() {
guard let graphicLoader else { return }
Task(priority: .high) { [unowned self, graphicLoader] in
await MainActor.run {
self.state = .loading
}
do {
let graphicContext = try graphicLoader()
graphicContext.delegate = self
await MainActor.run {
self.graphicContext = graphicContext
self.state = .loaded
}
} catch {
NSLog("[SketchNote] - \(error.localizedDescription)")
await MainActor.run {
self.state = .failed
}
}
}
}
func save(on managedObjectContext: NSManagedObjectContext) async {
// guard let board else { return }
// do {
// board.data = try JSONEncoder().encode(self)
// board.updatedAt = Date()
// try managedObjectContext.save()
// } catch {
// NSLog("[SketchNote] - \(error.localizedDescription)")
// }
}
func listen(on managedObjectContext: NSManagedObjectContext) {
Task(priority: .background) { [unowned self] in
for await _ in didUpdate.values {
await save(on: managedObjectContext)
}
}
}
}
// MARK: - Dimension
extension Canvas {
func updateTransform(on drawingView: DrawingView) {
let bounds = CGRect(origin: .zero, size: size)
let renderView = drawingView.renderView
let targetRect = drawingView.convert(drawingView.bounds, to: renderView)
let transform1 = bounds.transform(to: targetRect)
let transform2 = renderView.bounds.transform(to: CGRect(x: -1.0, y: -1.0, width: 2.0, height: 2.0))
let transform3 = CGAffineTransform.identity.translatedBy(x: 0, y: 1).scaledBy(x: 1, y: -1).translatedBy(x: 0, y: 1)
self.transform = simd_float4x4(transform1 * transform2 * transform3)
}
func updateClipBounds(_ scrollView: UIScrollView, on drawingView: DrawingView) {
let ratio = drawingView.ratio
let bounds = scrollView.convert(scrollView.bounds, to: drawingView)
clipBounds = CGRect(origin: bounds.origin.muliply(by: ratio), size: bounds.size.multiply(by: ratio))
}
}
// MARK: - Zoom Scale
extension Canvas {
func setZoomScale(_ zoomScale: CGFloat) {
self.zoomScale = zoomScale
}
}
// MARK: - Graphic Context
extension Canvas {
func beginTouch(at point: CGPoint, pen: Pen) -> Stroke {
graphicContext.beginStroke(at: point, pen: pen)
}
func moveTouch(to point: CGPoint) {
graphicContext.appendStroke(with: point)
}
func endTouch(at point: CGPoint) {
graphicContext.endStroke(at: point)
}
func setGraphicRenderType(_ renderType: GraphicContext.RenderType) {
graphicContext.renderType = renderType
}
func getNewlyAddedStroke() -> Stroke? {
graphicContext.strokes.last
}
}
// MARK: - Rendering
extension Canvas {
func renderGrid(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
var uniforms = GridUniforms(
ratio: size.width.float / 100,
zoom: zoomScale.float,
transform: transform
)
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<GridUniforms>.size)
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
gridContext.draw(device: device, renderEncoder: renderEncoder)
}
func renderGraphic(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
graphicContext.draw(device: device, renderEncoder: renderEncoder)
}
func renderViewPort(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
viewPortContext.setViewPortVertices()
viewPortContext.draw(device: device, renderEncoder: renderEncoder)
}
func renderViewPortUpdate(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
var uniforms = Uniforms(transform: transform)
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<Uniforms>.size)
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
viewPortContext.setViewPortUpdateVertices(from: clipBounds)
viewPortContext.draw(device: device, renderEncoder: renderEncoder)
}
func setUniformsBuffer(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
var uniforms = Uniforms(transform: transform)
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<Uniforms>.size)
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
}
}
// MARK: - State
extension Canvas {
enum State {
case initial
case loading
case loaded
case closing
case closed
case failed
}
}

View File

@@ -0,0 +1,134 @@
//
// PipelineStates.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
struct PipelineStates {
static func createGridPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
let device = renderer.device
let library = renderer.library
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_grid")
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_grid")
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
pipelineDescriptor.label = "Grid Pipeline State"
let attachment = pipelineDescriptor.colorAttachments[0]
attachment?.isBlendingEnabled = true
attachment?.rgbBlendOperation = .add
attachment?.sourceRGBBlendFactor = .one
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
attachment?.alphaBlendOperation = .add
attachment?.sourceAlphaBlendFactor = .sourceAlpha
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
}
static func createGraphicPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
let device = renderer.device
let library = renderer.library
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_graphic")
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_graphic")
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
pipelineDescriptor.label = "Graphic Pipeline State"
let attachment = pipelineDescriptor.colorAttachments[0]
attachment?.isBlendingEnabled = true
attachment?.rgbBlendOperation = .add
attachment?.sourceRGBBlendFactor = .sourceAlpha
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
attachment?.alphaBlendOperation = .add
attachment?.sourceAlphaBlendFactor = .one
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
}
static func createStrokePipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
let device = renderer.device
let library = renderer.library
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_stroke")
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_stroke")
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
pipelineDescriptor.label = "Stroke Pipeline State"
let attachment = pipelineDescriptor.colorAttachments[0]
attachment?.isBlendingEnabled = true
attachment?.rgbBlendOperation = .add
attachment?.sourceRGBBlendFactor = .sourceAlpha
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
attachment?.alphaBlendOperation = .add
attachment?.sourceAlphaBlendFactor = .one
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
}
static func createEraserPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
let device = renderer.device
let library = renderer.library
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_stroke")
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_stroke")
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
pipelineDescriptor.label = "Eraser Pipeline State"
let attachment = pipelineDescriptor.colorAttachments[0]
attachment?.isBlendingEnabled = true
attachment?.rgbBlendOperation = .add
attachment?.sourceRGBBlendFactor = .sourceAlpha
attachment?.destinationRGBBlendFactor = .one
attachment?.alphaBlendOperation = .reverseSubtract
attachment?.sourceAlphaBlendFactor = .sourceAlpha
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
}
static func createViewPortPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil, isUpdate: Bool = false) -> MTLRenderPipelineState? {
var label: String
var vertexName: String
if isUpdate {
label = "View Port Update Pipeline State"
vertexName = "vertex_viewport_update"
} else {
label = "View Port Pipeline State"
vertexName = "vertex_viewport"
}
let device = renderer.device
let library = renderer.library
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = library.makeFunction(name: vertexName)
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_viewport")
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
pipelineDescriptor.label = label
let attachment = pipelineDescriptor.colorAttachments[0]
attachment?.isBlendingEnabled = true
attachment?.rgbBlendOperation = .add
attachment?.sourceRGBBlendFactor = .sourceAlpha
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
attachment?.alphaBlendOperation = .add
attachment?.sourceAlphaBlendFactor = .one
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
}
static func createCachePipelineState(from renderer: Renderer) -> MTLComputePipelineState? {
let device = renderer.device
let library = renderer.library
guard let function = library.makeFunction(name: "copy_texture_viewport") else {
return nil
}
return try? device.makeComputePipelineState(function: function)
}
}

View File

@@ -0,0 +1,85 @@
//
// Renderer.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
final class Renderer {
var device: MTLDevice
var library: MTLLibrary
var pixelFormat: MTLPixelFormat
var commandQueue: MTLCommandQueue
var redrawsGraphicRender: Bool = true
var updatesViewPort: Bool = false
var canvasView: MTKView
lazy var strokeRenderPass: StrokeRenderPass = {
StrokeRenderPass(renderer: self)
}()
lazy var eraserRenderPass: EraserRenderPass = {
EraserRenderPass(renderer: self)
}()
lazy var graphicRenderPass: GraphicRenderPass = {
GraphicRenderPass(renderer: self)
}()
lazy var cacheRenderPass: CacheRenderPass = {
CacheRenderPass(renderer: self)
}()
lazy var viewPortRenderPass: ViewPortRenderPass = {
ViewPortRenderPass(renderer: self)
}()
init(canvasView: MTKView) {
guard let device = MTLCreateSystemDefaultDevice() else {
fatalError("[Error]: Unable to create system default device.")
}
guard let commandQueue = device.makeCommandQueue() else {
fatalError("[Error]: Unable to create command queue.")
}
guard let library = device.makeDefaultLibrary() else {
fatalError("[Error]: Unable to create default library.")
}
self.device = device
self.commandQueue = commandQueue
self.library = library
self.pixelFormat = canvasView.colorPixelFormat
self.canvasView = canvasView
canvasView.device = device
self.viewPortRenderPass.view = canvasView
}
func resize(on view: MTKView, to size: CGSize) {
if !updatesViewPort {
strokeRenderPass.resize(on: view, to: size, with: self)
graphicRenderPass.resize(on: view, to: size, with: self)
cacheRenderPass.resize(on: view, to: size, with: self)
}
viewPortRenderPass.resize(on: view, to: size, with: self)
redrawsGraphicRender = true
}
func draw(in view: MTKView, on canvas: Canvas) {
if !updatesViewPort {
graphicRenderPass.strokeRenderPass = strokeRenderPass
graphicRenderPass.eraserRenderPass = eraserRenderPass
graphicRenderPass.draw(on: canvas, with: self)
}
cacheRenderPass.clearsTexture = graphicRenderPass.clearsTexture
cacheRenderPass.strokeRenderPass = strokeRenderPass
cacheRenderPass.eraserRenderPass = eraserRenderPass
cacheRenderPass.graphicTexture = graphicRenderPass.graphicTexture
cacheRenderPass.graphicPipelineState = graphicRenderPass.graphicPipelineState
cacheRenderPass.draw(on: canvas, with: self)
viewPortRenderPass.descriptor = view.currentRenderPassDescriptor
viewPortRenderPass.cacheTexture = cacheRenderPass.cacheTexture
viewPortRenderPass.draw(on: canvas, with: self)
}
}

View File

@@ -0,0 +1,93 @@
//
// Textures.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
class Textures {
static var penTextures: [String: MTLTexture] = [:]
@discardableResult
static func createPenTexture(with textureName: String, on device: MTLDevice) -> MTLTexture? {
if let penTexture = penTextures[textureName] {
return penTexture
}
let textureLoader = MTKTextureLoader(device: device)
let penTexture = try? textureLoader.newTexture(name: textureName, scaleFactor: 1.0, bundle: .main, options: [.SRGB: false])
penTextures[textureName] = penTexture
return penTexture
}
static func createGraphicTexture(
from renderer: Renderer,
size: CGSize,
pixelFormat: MTLPixelFormat? = nil
) -> MTLTexture? {
let width = Int(size.width)
let height = Int(size.height)
guard width > 0, height > 0 else { return nil }
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: pixelFormat ?? renderer.pixelFormat,
width: width,
height: height,
mipmapped: false
)
descriptor.storageMode = .shared
descriptor.usage = [.shaderRead, .renderTarget, .shaderWrite]
guard let texture = renderer.device.makeTexture(descriptor: descriptor) else {
return nil
}
texture.label = "Graphic Texture"
return texture
}
static func createCacheTexture(
from renderer: Renderer,
size: CGSize,
pixelFormat: MTLPixelFormat? = nil
) -> MTLTexture? {
let width = Int(size.width)
let height = Int(size.height)
guard width > 0, height > 0 else { return nil }
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: pixelFormat ?? renderer.pixelFormat,
width: width,
height: height,
mipmapped: false
)
descriptor.storageMode = .shared
descriptor.usage = [.shaderRead, .renderTarget, .shaderWrite]
guard let texture = renderer.device.makeTexture(descriptor: descriptor) else {
return nil
}
texture.label = "Cache Texture"
return texture
}
static func createStrokeTexture(
from renderer: Renderer,
size: CGSize,
pixelFormat: MTLPixelFormat? = nil
) -> MTLTexture? {
let width = Int(size.width)
let height = Int(size.height)
guard width > 0, height > 0 else { return nil }
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: pixelFormat ?? renderer.pixelFormat,
width: width,
height: height,
mipmapped: false
)
descriptor.storageMode = .shared
descriptor.usage = [.shaderRead, .renderTarget, .shaderWrite]
guard let texture = renderer.device.makeTexture(descriptor: descriptor) else {
return nil
}
texture.label = "Stroke Texture"
return texture
}
}

View File

@@ -0,0 +1,106 @@
//
// Quad.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
class Quad {
var origin: CGPoint
var color: [CGFloat]
var size: CGFloat
var rotation: CGFloat
var vertices: [QuadVertex] = []
var vertexBuffer: MTLBuffer?
var vertexCount: Int = 0
init(origin: CGPoint, size: CGFloat, color: [CGFloat], rotation: CGFloat, shape: Shape = .rounded) {
self.origin = origin
self.size = size
self.color = color
self.rotation = rotation
generateVertices(shape)
}
func generateVertices(_ shape: Shape) {
switch shape {
case .rounded:
generateRoundedQuad()
case .squared:
generateSquaredQuad()
case let .calligraphic(vFactor, hFactor):
generateCalligraphicQuad(vFactor: vFactor, hFactor: hFactor)
case let .trapezoid(topFactor, bottomFactor, heightFactor):
generateTrapezoidQuad(topFactor: topFactor, bottomFactor: bottomFactor, heightFactor: heightFactor)
}
}
func generateRoundedQuad() {
let halfSize = size * 0.5
vertices = [
QuadVertex(x: origin.x - halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
]
vertexCount = vertices.count
}
func generateSquaredQuad() {
let vHalfSize = size * 0.5
let hHalfSize = size * 0.15
vertices = [
QuadVertex(x: origin.x - hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
]
vertexCount = vertices.count
}
func generateCalligraphicQuad(vFactor: CGFloat, hFactor: CGFloat) {
let vHalfSize = size * vFactor * 0.5
let hHalfSize = size * hFactor * 0.5
vertices = [
QuadVertex(x: origin.x - hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
]
vertexCount = vertices.count
}
func generateTrapezoidQuad(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) {
let vHalfSize = size * heightFactor * 0.5
let hTopHalfSize = size * topFactor * 0.5
let hBottomHalfSize = size * bottomFactor * 0.5
vertices = [
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
]
vertexCount = vertices.count
}
}
extension Quad {
enum Shape {
case rounded
case squared
case calligraphic(CGFloat, CGFloat)
case trapezoid(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat)
}
}

View File

@@ -0,0 +1,154 @@
//
// SolidPointStrokeGenerator.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Foundation
struct SolidPointStrokeGenerator: StrokeGenerator {
var configuration: Configuration
func begin(at point: CGPoint, on stroke: Stroke) {
stroke.keyPoints.append(point)
addPoint(point, on: stroke)
}
func append(to point: CGPoint, on stroke: Stroke) {
guard stroke.keyPoints.count > 0 else {
return
}
stroke.keyPoints.append(point)
switch stroke.keyPoints.count {
case 2:
let start = stroke.keyPoints[0]
let end = stroke.keyPoints[1]
let control = CGPoint.middle(p1: start, p2: end)
addCurve(from: start, to: end, by: control, on: stroke)
case 3:
discardPoints(upto: stroke.vertexIndex, on: stroke)
let index = stroke.keyPoints.count - 1
var start = stroke.keyPoints[index - 2]
var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
var control = CGPoint.middle(p1: start, p2: end)
addCurve(from: start, to: end, by: control, on: stroke)
start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
control = stroke.keyPoints[index - 1]
end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
addCurve(from: start, to: end, by: control, on: stroke)
default:
smoothOutPath(on: stroke)
let index = stroke.keyPoints.count - 1
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
let control = stroke.keyPoints[index - 1]
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
addCurve(from: start, to: end, by: control, on: stroke)
}
}
func finish(at point: CGPoint, on stroke: Stroke) {
switch stroke.keyPoints.count {
case 0...1:
break
default:
append(to: point, on: stroke)
let index = stroke.keyPoints.count - 1
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
let end = stroke.keyPoints[index]
let control = CGPoint.middle(p1: start, p2: end)
addCurve(from: start, to: end, by: control, on: stroke)
}
}
private func smoothOutPath(on stroke: Stroke) {
discardPoints(upto: stroke.vertexIndex, on: stroke)
adjustPreviousKeyPoint(on: stroke)
switch stroke.keyPoints.count {
case 4:
let index = stroke.keyPoints.count - 2
let start = stroke.keyPoints[index - 2]
let end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
let control = CGPoint.middle(p1: start, p2: end)
addCurve(from: start, to: end, by: control, on: stroke)
fallthrough
default:
let index = stroke.keyPoints.count - 2
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
let control = stroke.keyPoints[index - 1]
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
addCurve(from: start, to: end, by: control, on: stroke)
}
stroke.vertexIndex = stroke.vertices.endIndex - 1
}
private func adjustPreviousKeyPoint(on stroke: Stroke) {
let index = stroke.keyPoints.count - 1
let prev = stroke.keyPoints[index - 1]
let current = stroke.keyPoints[index]
let averageX = (prev.x + current.x) / 2
let averageY = (prev.y + current.y) / 2
let point = CGPoint(x: averageX, y: averageY)
stroke.keyPoints[index] = point
stroke.keyPoints[index - 1] = point
}
private func addPoint(_ point: CGPoint, on stroke: Stroke) {
let rotation: CGFloat
switch configuration.rotation {
case .fixed:
rotation = 0
case .random:
rotation = CGFloat.random(in: 0...360) * .pi / 180
}
let quad = Quad(origin: point, size: stroke.thickness, color: stroke.color, rotation: rotation)
stroke.vertices.append(contentsOf: quad.vertices)
stroke.vertexCount = stroke.vertices.endIndex
}
private func addCurve(from start: CGPoint, to end: CGPoint, by control: CGPoint, on stroke: Stroke) {
let distance = start.distance(to: end)
let factor: CGFloat
switch configuration.granularity {
case .fixed:
factor = 1 / (stroke.thickness * stroke.style.stepRate)
case .none:
factor = 1 / (stroke.thickness * 10 / 500)
}
let segements = max(Int(distance * factor), 1)
for i in 0..<segements {
let t = CGFloat(i) / CGFloat(segements)
let x = pow(1 - t, 2) * start.x + 2.0 * (1 - t) * t * control.x + t * t * end.x
let y = pow(1 - t, 2) * start.y + 2.0 * (1 - t) * t * control.y + t * t * end.y
let point = CGPoint(x: x, y: y)
addPoint(point, on: stroke)
}
}
private func discardPoints(upto index: Int, on stroke: Stroke) {
if index < 0 {
stroke.vertices.removeAll()
} else {
let count = stroke.vertices.endIndex
let dropCount = count - (max(0, index) + 1)
stroke.vertices.removeLast(dropCount)
}
}
}
extension SolidPointStrokeGenerator {
struct Configuration {
var rotation: Rotation = .fixed
var granularity: Granularity = .none
}
enum Rotation {
case fixed
case random
}
enum Granularity {
case fixed
case none
}
}

View File

@@ -0,0 +1,112 @@
//
// Stroke.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
class Stroke: Codable {
var color: [CGFloat]
var style: any PenStyle
var thickness: CGFloat
var angle: CGFloat = 0
var vertexIndex: Int = -1
var keyPoints: [CGPoint] = []
var thicknessFactor: CGFloat = 0.7
var vertices: [QuadVertex] = []
var vertexBuffer: MTLBuffer?
var vertexCount: Int = 0
var tailVertices: [QuadVertex] = []
var texture: MTLTexture?
// var strokeTexture: MTLTexture?
var isEmpty: Bool {
vertices.isEmpty
}
var isEraserPenStyle: Bool {
style is EraserPenStyle
}
init(color: [CGFloat], style: any PenStyle, thickness: CGFloat) {
self.color = color
self.style = style
self.thickness = thickness
}
enum CodingKeys: CodingKey {
case color
case style
case thickness
case vertices
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
color = try container.decode([CGFloat].self, forKey: .color)
let style: String = try container.decode(String.self, forKey: .style)
thickness = try container.decode(CGFloat.self, forKey: .thickness)
vertices = try container.decode([QuadVertex].self, forKey: .vertices)
vertexCount = vertices.count
switch style {
case "marker":
self.style = .marker
case "eraser":
self.style = .eraser
default:
throw DecodingError.valueNotFound(PenStyle.self, .init(codingPath: [CodingKeys.style], debugDescription: "There is no pen style called `\(style)`."))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(color, forKey: .color)
try container.encode(thickness, forKey: .thickness)
try container.encode(vertices, forKey: .vertices)
let styleName: String
switch style {
case is MarkerPenStyle:
styleName = "marker"
case is EraserPenStyle:
styleName = "eraser"
default:
fatalError()
}
try container.encode(styleName, forKey: .style)
}
func begin(at point: CGPoint) {
style.generator.begin(at: point, on: self)
}
func append(to point: CGPoint) {
style.generator.append(to: point, on: self)
}
func finish(at point: CGPoint) {
style.generator.finish(at: point, on: self)
}
}
extension Stroke: Drawable {
func prepare(device: MTLDevice) {
if texture == nil {
texture = style.loadTexture(on: device)
}
vertexBuffer = device.makeBuffer(bytes: &vertices, length: MemoryLayout<QuadVertex>.stride * vertexCount, options: .cpuCacheModeWriteCombined)
}
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
guard !isEmpty else { return }
prepare(device: device)
renderEncoder.setFragmentTexture(texture, index: 0)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount)
}
}

View File

@@ -0,0 +1,18 @@
//
// StrokeGenerator.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Foundation
protocol StrokeGenerator {
associatedtype Configuration
var configuration: Configuration { get set }
func begin(at point: CGPoint, on stroke: Stroke)
func append(to point: CGPoint, on stroke: Stroke)
func finish(at point: CGPoint, on stroke: Stroke)
}

View File

@@ -0,0 +1,51 @@
//
// History.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Combine
import Foundation
class History: ObservableObject {
@Published var undoStack: [HistoryEvent] = []
@Published var redoStack: [HistoryEvent] = []
let historyPublisher = PassthroughSubject<HistoryAction, Never>()
var undoDisabled: Bool {
undoStack.isEmpty
}
var redoDisabled: Bool {
redoStack.isEmpty
}
func undo() -> Bool {
guard let event = undoStack.popLast() else {
return false
}
addRedo(event)
return true
}
func redo() -> HistoryEvent? {
guard let event = redoStack.popLast() else {
return nil
}
addUndo(event)
return event
}
func addUndo(_ event: HistoryEvent) {
undoStack.append(event)
}
func addRedo(_ event: HistoryEvent) {
redoStack.append(event)
}
func resetRedo() {
redoStack.removeAll()
}
}

View File

@@ -0,0 +1,13 @@
//
// HistoryAction.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Foundation
enum HistoryAction {
case undo
case redo
}

View File

@@ -0,0 +1,12 @@
//
// HistoryEvent.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Foundation
enum HistoryEvent {
case stroke(Stroke)
}

View File

@@ -0,0 +1,87 @@
//
// CacheRenderPass.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
class CacheRenderPass: RenderPass {
var label: String = "Cache Render Pass"
var descriptor: MTLRenderPassDescriptor?
var cachePipelineState: MTLComputePipelineState?
var graphicPipelineState: MTLRenderPipelineState?
weak var graphicTexture: MTLTexture?
var cacheTexture: MTLTexture?
weak var strokeRenderPass: StrokeRenderPass?
weak var eraserRenderPass: EraserRenderPass?
var clearsTexture: Bool = true
init(renderer: Renderer) {
descriptor = MTLRenderPassDescriptor()
cachePipelineState = PipelineStates.createCachePipelineState(from: renderer)
}
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) {
guard size != .zero else { return }
cacheTexture = Textures.createCacheTexture(from: renderer, size: size, pixelFormat: view.colorPixelFormat)
}
func draw(on canvas: Canvas, with renderer: Renderer) {
guard let descriptor, let strokeRenderPass, let eraserRenderPass else { return }
copyTexture(on: canvas, with: renderer)
guard let graphicPipelineState else { return }
descriptor.colorAttachments[0].texture = cacheTexture
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
descriptor.colorAttachments[0].storeAction = .store
if let stroke = canvas.graphicContext.currentStroke {
if stroke.isEraserPenStyle {
eraserRenderPass.stroke = stroke
eraserRenderPass.descriptor = descriptor
eraserRenderPass.draw(on: canvas, with: renderer)
} else {
canvas.setGraphicRenderType(.inProgress)
strokeRenderPass.stroke = stroke
strokeRenderPass.graphicDescriptor = descriptor
strokeRenderPass.graphicPipelineState = graphicPipelineState
strokeRenderPass.draw(on: canvas, with: renderer)
}
clearsTexture = false
}
}
private func copyTexture(on canvas: Canvas, with renderer: Renderer) {
guard let graphicTexture, let cacheTexture else { return }
guard let cachePipelineState else { return }
guard let copyCommandBuffer = renderer.commandQueue.makeCommandBuffer() else {
return
}
guard let computeEncoder = copyCommandBuffer.makeComputeCommandEncoder() else {
return
}
computeEncoder.label = label
computeEncoder.setComputePipelineState(cachePipelineState)
computeEncoder.setTexture(graphicTexture, index: 0)
computeEncoder.setTexture(cacheTexture, index: 1)
let threadgroupSize = MTLSize(width: 8, height: 8, depth: 1)
let threadgroupCount = MTLSize(
width: (graphicTexture.width + threadgroupSize.width - 1) / threadgroupSize.width,
height: (graphicTexture.height + threadgroupSize.height - 1) / threadgroupSize.height,
depth: 1
)
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.endEncoding()
copyCommandBuffer.commit()
}
}

View File

@@ -0,0 +1,45 @@
//
// EraserRenderPass.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
class EraserRenderPass: RenderPass {
var label: String = "Eraser Render Pass"
var descriptor: MTLRenderPassDescriptor?
var eraserPipelineState: MTLRenderPipelineState?
var stroke: Stroke?
weak var graphicTexture: MTLTexture?
init(renderer: Renderer) {
eraserPipelineState = PipelineStates.createEraserPipelineState(from: renderer)
}
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { }
func draw(on canvas: Canvas, with renderer: Renderer) {
guard let descriptor else { return }
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
commandBuffer.label = "Eraser Command Buffer"
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return }
renderEncoder.label = label
guard let eraserPipelineState else { return }
renderEncoder.setRenderPipelineState(eraserPipelineState)
canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder)
stroke?.draw(device: renderer.device, renderEncoder: renderEncoder)
renderEncoder.endEncoding()
commandBuffer.commit()
}
}

View File

@@ -0,0 +1,86 @@
//
// GraphicRenderPass.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
class GraphicRenderPass: RenderPass {
var label: String { "Graphic Render Pass" }
var descriptor: MTLRenderPassDescriptor?
var graphicTexture: MTLTexture?
var graphicPipelineState: MTLRenderPipelineState?
weak var strokeRenderPass: StrokeRenderPass?
weak var eraserRenderPass: EraserRenderPass?
var clearsTexture: Bool = true
init(renderer: Renderer) {
descriptor = MTLRenderPassDescriptor()
graphicPipelineState = PipelineStates.createGraphicPipelineState(from: renderer)
}
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) {
guard size != .zero else { return }
graphicTexture = Textures.createGraphicTexture(from: renderer, size: size, pixelFormat: view.colorPixelFormat)
clearsTexture = true
}
func draw(on canvas: Canvas, with renderer: Renderer) {
guard let strokeRenderPass, let eraserRenderPass else { return }
guard let descriptor else { return }
guard let graphicPipelineState else { return }
let graphicContext = canvas.graphicContext
guard let graphicTexture else { return }
descriptor.colorAttachments[0].texture = graphicTexture
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
descriptor.colorAttachments[0].storeAction = .store
if renderer.redrawsGraphicRender {
canvas.setGraphicRenderType(.finished)
for stroke in graphicContext.strokes {
if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
continue
}
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
clearsTexture = false
if stroke.isEraserPenStyle {
eraserRenderPass.stroke = stroke
eraserRenderPass.descriptor = descriptor
eraserRenderPass.draw(on: canvas, with: renderer)
} else {
canvas.setGraphicRenderType(.finished)
strokeRenderPass.stroke = stroke
strokeRenderPass.graphicDescriptor = descriptor
strokeRenderPass.graphicPipelineState = graphicPipelineState
strokeRenderPass.draw(on: canvas, with: renderer)
}
}
renderer.redrawsGraphicRender = false
}
if let stroke = graphicContext.previousStroke {
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
clearsTexture = false
if stroke.isEraserPenStyle {
eraserRenderPass.stroke = stroke
eraserRenderPass.descriptor = descriptor
eraserRenderPass.draw(on: canvas, with: renderer)
} else {
canvas.setGraphicRenderType(.newlyFinished)
strokeRenderPass.stroke = stroke
strokeRenderPass.graphicDescriptor = descriptor
strokeRenderPass.graphicPipelineState = graphicPipelineState
strokeRenderPass.draw(on: canvas, with: renderer)
}
graphicContext.previousStroke = nil
}
}
}

View File

@@ -0,0 +1,78 @@
//
// StrokeRenderPass.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
class StrokeRenderPass: RenderPass {
var label: String = "Stroke Render Pass"
var descriptor: MTLRenderPassDescriptor?
weak var graphicDescriptor: MTLRenderPassDescriptor?
var strokePipelineState: MTLRenderPipelineState?
weak var graphicPipelineState: MTLRenderPipelineState?
var stroke: Stroke?
var strokeTexture: MTLTexture?
init(renderer: Renderer) {
descriptor = MTLRenderPassDescriptor()
strokePipelineState = PipelineStates.createStrokePipelineState(from: renderer)
}
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) {
guard size != .zero else { return }
strokeTexture = Textures.createStrokeTexture(from: renderer, size: size, pixelFormat: view.colorPixelFormat)
}
func draw(on canvas: Canvas, with renderer: Renderer) {
guard let descriptor else { return }
guard let strokeTexture else { return }
descriptor.colorAttachments[0].texture = strokeTexture
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
descriptor.colorAttachments[0].loadAction = .clear
descriptor.colorAttachments[0].storeAction = .store
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
commandBuffer.label = "Stroke Command Buffer"
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return }
renderEncoder.label = label
guard let strokePipelineState else { return }
renderEncoder.setRenderPipelineState(strokePipelineState)
canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder)
stroke?.draw(device: renderer.device, renderEncoder: renderEncoder)
renderEncoder.endEncoding()
commandBuffer.commit()
drawStrokeTexture(on: canvas, with: renderer)
}
func drawStrokeTexture(on canvas: Canvas, with renderer: Renderer) {
guard let stroke else { return }
guard let graphicDescriptor, let graphicPipelineState else { return }
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
commandBuffer.label = "Graphic Command Buffer"
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: graphicDescriptor) else { return }
renderEncoder.label = "Graphic Render Pass"
renderEncoder.setRenderPipelineState(graphicPipelineState)
renderEncoder.setFragmentTexture(strokeTexture, index: 0)
var uniforms = GraphicUniforms(color: stroke.color)
let uniformsBuffer = renderer.device.makeBuffer(bytes: &uniforms, length: MemoryLayout<Uniforms>.size)
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
canvas.renderGraphic(device: renderer.device, renderEncoder: renderEncoder)
renderEncoder.endEncoding()
commandBuffer.commit()
}
}

View File

@@ -0,0 +1,74 @@
//
// ViewPortRenderPass.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
class ViewPortRenderPass: RenderPass {
var label: String { "View Port Render Pass"}
var descriptor: MTLRenderPassDescriptor?
var gridPipelineState: MTLRenderPipelineState?
var viewPortPipelineState: MTLRenderPipelineState?
var viewPortUpdatePipelineState: MTLRenderPipelineState?
weak var cacheTexture: MTLTexture?
weak var view: MTKView?
init(renderer: Renderer) {
gridPipelineState = PipelineStates.createGridPipelineState(from: renderer)
viewPortPipelineState = PipelineStates.createViewPortPipelineState(from: renderer)
viewPortUpdatePipelineState = PipelineStates.createViewPortPipelineState(from: renderer, isUpdate: true)
}
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { }
func draw(on canvas: Canvas, with renderer: Renderer) {
guard let descriptor else {
return
}
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else {
return
}
commandBuffer.label = "View Port Command Buffer"
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
return
}
renderEncoder.label = label
guard let gridPipelineState else { return }
renderEncoder.setRenderPipelineState(gridPipelineState)
canvas.renderGrid(device: renderer.device, renderEncoder: renderEncoder)
if renderer.updatesViewPort {
guard let viewPortUpdatePipelineState else {
return
}
renderEncoder.setRenderPipelineState(viewPortUpdatePipelineState)
renderEncoder.setFragmentTexture(cacheTexture, index: 0)
canvas.renderViewPortUpdate(device: renderer.device, renderEncoder: renderEncoder)
} else {
guard let viewPortPipelineState else {
return
}
renderEncoder.setRenderPipelineState(viewPortPipelineState)
renderEncoder.setFragmentTexture(cacheTexture, index: 0)
canvas.renderViewPort(device: renderer.device, renderEncoder: renderEncoder)
}
renderEncoder.endEncoding()
guard let drawable = view?.currentDrawable else {
return
}
commandBuffer.present(drawable)
commandBuffer.commit()
}
}

View File

@@ -0,0 +1,18 @@
//
// Cache.metal
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
#include <metal_stdlib>
using namespace metal;
kernel void copy_texture_viewport(
texture2d<float, access::read> inTexture [[texture(0)]],
texture2d<float, access::write> outTexture [[texture(1)]],
uint2 gid [[thread_position_in_grid]]
) {
float4 color = inTexture.read(gid);
outTexture.write(color, gid);
}

View File

@@ -0,0 +1,46 @@
//
// Graphic.metal
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
#include <metal_stdlib>
using namespace metal;
struct VertexIn {
float4 position [[position]];
float2 textCoord;
};
struct VertexOut {
float4 position [[position]];
float2 textCoord;
float4 color;
};
struct Uniforms {
float4 color;
};
vertex VertexOut vertex_graphic(
constant VertexIn *vertices [[buffer(0)]],
constant Uniforms &uniforms [[buffer(11)]],
uint vertexId [[vertex_id]]
) {
VertexIn in = vertices[vertexId];
VertexOut out;
out.position = in.position;
out.textCoord = in.textCoord;
out.color = uniforms.color;
return out;
}
fragment float4 fragment_graphic(
VertexOut out [[stage_in]],
texture2d<float> offscreenTexture [[texture(0)]]
) {
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
float4 color = float4(offscreenTexture.sample(textureSampler, out.textCoord));
return float4(out.color.rgb, color.a * out.color.a);
}

View File

@@ -0,0 +1,44 @@
//
// Grid.metal
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
#include <metal_stdlib>
using namespace metal;
struct Vertex {
float4 position [[position]];
float pointSize [[point_size]];
};
struct Uniforms {
float ratio;
float zoom;
float4x4 transform;
};
vertex Vertex vertex_grid(
constant Vertex *vertices [[buffer(0)]],
constant Uniforms &uniforms [[buffer(11)]],
uint vertexId [[vertex_id]]
) {
Vertex _vertex = vertices[vertexId];
float x = _vertex.position.x * uniforms.ratio;
float y = _vertex.position.y * uniforms.ratio;
float4 position = float4(x, y, 0, 1);
_vertex.position = uniforms.transform * position;
_vertex.pointSize = 10 * uniforms.zoom / 12;
return _vertex;
}
fragment float4 fragment_grid(
Vertex _vertex [[stage_in]],
float2 pointCoord [[point_coord]]
) {
float dist = length(pointCoord - float2(0.5));
float4 color = float4(0.752, 0.752, 0.752, 1);
color.a = 1.0 - smoothstep(0.4, 0.5, dist);
return color;
}

View File

@@ -0,0 +1,55 @@
//
// Stroke.metal
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
#include <metal_stdlib>
using namespace metal;
struct VertexIn {
float4 position [[position]];
float2 textCoord;
float4 color;
float2 origin;
float rotation;
};
struct VertexOut {
float4 position [[position]];
float2 textCoord;
float4 color;
};
struct Uniforms {
float4x4 transform;
};
vertex VertexOut vertex_stroke(
constant VertexIn *vertices [[buffer(0)]],
constant Uniforms &uniforms [[buffer(11)]],
uint vertexId [[vertex_id]]
) {
VertexIn in = vertices[vertexId];
float2 rotatedPosition;
rotatedPosition.x = cos(in.rotation) * (in.position.x - in.origin.x) - sin(in.rotation) * (in.position.y - in.origin.y) + in.origin.x;
rotatedPosition.y = sin(in.rotation) * (in.position.x - in.origin.x) + cos(in.rotation) * (in.position.y - in.origin.y) + in.origin.y;
VertexOut out;
out.position = uniforms.transform * float4(rotatedPosition, 0, 1);
out.textCoord = in.textCoord;
out.color = in.color;
return out;
}
fragment float4 fragment_stroke(
VertexOut out [[stage_in]],
texture2d<float> texture [[texture(0)]]
) {
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
float4 color = float4(texture.sample(textureSampler, out.textCoord));
return float4(1, 1, 1, color.a);
}

View File

@@ -0,0 +1,45 @@
//
// Viewport.metal
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
#include <metal_stdlib>
using namespace metal;
struct Vertex {
float4 position [[position]];
float2 textCoord;
};
struct Uniforms {
float4x4 transform;
};
vertex Vertex vertex_viewport(
constant Vertex *vertices [[buffer(0)]],
uint vertexId [[vertex_id]]
) {
Vertex _vertex = vertices[vertexId];
return _vertex;
}
vertex Vertex vertex_viewport_update(
constant Vertex *vertices [[buffer(0)]],
constant Uniforms &uniforms [[buffer(11)]],
uint vertexId [[vertex_id]]
) {
Vertex _vertex = vertices[vertexId];
_vertex.position = uniforms.transform * _vertex.position;
return _vertex;
}
fragment float4 fragment_viewport(
Vertex _vertex [[stage_in]],
texture2d<float> offscreenTexture [[texture(0)]]
) {
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
float4 sampledColor = float4(offscreenTexture.sample(textureSampler, _vertex.textCoord));
return sampledColor;
}

View File

@@ -0,0 +1,27 @@
//
// Pen.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import SwiftUI
import Foundation
class Pen: NSObject, ObservableObject, Identifiable {
@Published var style: PenStyle
@Published var color: [CGFloat]
@Published var thickness: CGFloat
init(style: any PenStyle, color: [CGFloat], thickness: CGFloat) {
self.style = style
self.color = color
self.thickness = thickness
}
}
extension Pen {
convenience init(for style: any PenStyle) {
self.init(style: style, color: style.color, thickness: style.thinkness.min)
}
}

View File

@@ -0,0 +1,25 @@
//
// PenStyle.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
protocol PenStyle {
var icon: (base: String, tip: String?) { get }
var textureName: String { get }
var thinkness: (min: CGFloat, max: CGFloat) { get }
var color: [CGFloat] { get }
var stepRate: CGFloat { get }
var generator: any StrokeGenerator { get }
}
extension PenStyle {
@discardableResult
func loadTexture(on device: MTLDevice) -> MTLTexture? {
Textures.createPenTexture(with: textureName, on: device)
}
}

View File

@@ -0,0 +1,30 @@
//
// EraserPenStyle.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Foundation
struct EraserPenStyle: PenStyle {
var icon: (base: String, tip: String?) = ("eraser", nil)
var textureName: String = "point-texture"
var thinkness: (min: CGFloat, max: CGFloat) = (15, 120)
var color: [CGFloat] = [1, 1, 1, 0]
var stepRate: CGFloat = 0.2
var generator: any StrokeGenerator {
SolidPointStrokeGenerator(configuration: .init())
}
}
extension PenStyle where Self == EraserPenStyle {
static var eraser: PenStyle {
EraserPenStyle()
}
}

View File

@@ -0,0 +1,30 @@
//
// MarkerPenStyle.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Foundation
struct MarkerPenStyle: PenStyle {
var icon: (base: String, tip: String?) = ("marker-base", "marker-tip")
var textureName: String = "point-texture"
var thinkness: (min: CGFloat, max: CGFloat) = (15, 120)
var color: [CGFloat] = [1, 0.38, 0.38, 1]
var stepRate: CGFloat = 0.2
var generator: any StrokeGenerator {
SolidPointStrokeGenerator(configuration: .init())
}
}
extension PenStyle where Self == MarkerPenStyle {
static var marker: PenStyle {
MarkerPenStyle()
}
}

View File

@@ -0,0 +1,26 @@
//
// Tool.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import SwiftUI
import Foundation
class Tool: NSObject, ObservableObject {
@Published var pens: [Pen]
@Published var selectedPen: Pen?
override init() {
pens = [
Pen(for: .marker),
Pen(for: .eraser)
]
super.init()
}
func changePen(_ pen: Pen) {
selectedPen = pen
}
}

View File

@@ -0,0 +1,331 @@
//
// CanvasViewController.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Combine
import SwiftUI
import MetalKit
import Foundation
class CanvasViewController: UIViewController {
let drawingView: DrawingView
let scrollView: UIScrollView = UIScrollView()
var renderView: MTKView {
drawingView.renderView
}
let tool: Tool
let canvas: Canvas
let history: History
let renderer: Renderer
var cancellables: Set<AnyCancellable> = []
init(tool: Tool, canvas: Canvas, history: History) {
self.tool = tool
self.canvas = canvas
self.history = history
self.drawingView = DrawingView(tool: tool, canvas: canvas, history: history)
self.renderer = Renderer(canvasView: drawingView.renderView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
configureViews()
configureGestures()
configureListeners()
loadBoard()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
resizeDocumentView()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
drawingView.disableUserInteraction()
drawingView.updateDrawableSize(with: view.frame.size)
renderer.resize(on: renderView, to: renderView.drawableSize)
renderView.draw()
drawingView.enableUserInteraction()
}
}
extension CanvasViewController {
func configureViews() {
view.backgroundColor = .white
renderView.autoResizeDrawable = false
renderView.enableSetNeedsDisplay = true
renderView.translatesAutoresizingMaskIntoConstraints = false
renderView.clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 1)
view.addSubview(renderView)
NSLayoutConstraint.activate([
renderView.topAnchor.constraint(equalTo: view.topAnchor),
renderView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
renderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
renderView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
scrollView.maximumZoomScale = canvas.maximumZoomScale
scrollView.minimumZoomScale = canvas.minimumZoomScale
scrollView.contentInsetAdjustmentBehavior = .never
scrollView.isScrollEnabled = true
scrollView.showsVerticalScrollIndicator = true
scrollView.showsHorizontalScrollIndicator = true
scrollView.delegate = self
scrollView.backgroundColor = .clear
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
scrollView.addSubview(drawingView)
drawingView.backgroundColor = .clear
drawingView.isUserInteractionEnabled = false
}
func resizeDocumentView(to newSize: CGSize? = nil) {
scrollView.layoutIfNeeded()
let size = canvas.size
let widthScale = (newSize?.width ?? view.frame.width) / size.width
let heightScale = (newSize?.height ?? view.frame.height) / size.height
let scale = max(widthScale, heightScale)
let width = size.width * scale
let height = size.height * scale
let newFrame = CGRect(x: 0, y: 0, width: width, height: height)
drawingView.frame = newFrame
scrollView.setZoomScale(canvas.minimumZoomScale, animated: true)
centerDocumentView(to: newSize)
let offsetX = (newFrame.width * canvas.minimumZoomScale - view.frame.width) / 2
let offsetY = (newFrame.height * canvas.minimumZoomScale - view.frame.height) / 2
let point = CGPoint(x: offsetX, y: offsetY)
scrollView.setContentOffset(point, animated: true)
drawingView.updateDrawableSize(with: view.frame.size)
}
func centerDocumentView(to newSize: CGSize? = nil) {
let documentViewSize = drawingView.frame.size
let scrollViewSize = newSize ?? view.frame.size
let verticalPadding = documentViewSize.height < scrollViewSize.height ? (scrollViewSize.height - documentViewSize.height) / 2 : 0
let horizontalPadding = documentViewSize.width < scrollViewSize.width ? (scrollViewSize.width - documentViewSize.width) / 2 : 0
self.scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
}
}
extension CanvasViewController {
func configureListeners() {
canvas.$state
.sink { [weak self] state in
self?.canvasStateChanged(state)
}
.store(in: &cancellables)
tool.$selectedPen
.sink { [weak self] pen in
self?.penChanged(to: pen)
}
.store(in: &cancellables)
history.historyPublisher
.sink { [weak self] action in
switch action {
case .undo:
self?.historyUndid()
case .redo:
self?.historyRedid()
}
}
.store(in: &cancellables)
}
}
extension CanvasViewController {
func loadBoard() {
canvas.load()
}
func canvasStateChanged(_ state: Canvas.State) {
guard state == .loaded else { return }
renderView.delegate = self
renderer.resize(on: renderView, to: renderView.drawableSize)
renderView.draw()
}
}
extension CanvasViewController: MTKViewDelegate {
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { }
func draw(in view: MTKView) {
guard view.drawableSize != .zero else {
return
}
canvas.updateTransform(on: drawingView)
renderer.draw(in: view, on: canvas)
}
}
extension CanvasViewController {
func configureGestures() {
let drawingPanGesture = UIPanGestureRecognizer(target: self, action: #selector(recognizePanGesture))
drawingPanGesture.maximumNumberOfTouches = 1
drawingPanGesture.minimumNumberOfTouches = 1
drawingView.addGestureRecognizer(drawingPanGesture)
let drawingTapGesture = UITapGestureRecognizer(target: self, action: #selector(recognizeTapGesture))
drawingTapGesture.numberOfTapsRequired = 1
drawingView.addGestureRecognizer(drawingTapGesture)
}
@objc func recognizePanGesture(_ gesture: UIPanGestureRecognizer) {
let point = gesture.location(in: drawingView)
switch gesture.state {
case .began:
drawingView.touchBegan(on: point)
case .changed:
drawingView.touchMoved(to: point)
case .ended:
drawingView.touchEnded(to: point)
case .cancelled:
drawingView.touchEnded(to: point)
default:
break
}
}
@objc func recognizeTapGesture(_ gesture: UITapGestureRecognizer) {
let point = gesture.location(in: drawingView)
drawingView.touchBegan(on: point)
drawingView.touchEnded(to: point)
}
}
extension CanvasViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
drawingView
}
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
magnificationStarted()
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
canvas.setZoomScale(scrollView.zoomScale)
renderer.resize(on: renderView, to: renderView.drawableSize)
renderView.draw()
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
centerDocumentView()
magnificationEnded()
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
draggingStarted()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
renderer.resize(on: renderView, to: renderView.drawableSize)
renderView.draw()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if !scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating {
scrollViewDidEndScrolling(scrollView)
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate, scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating {
scrollViewDidEndScrolling(scrollView)
}
}
func scrollViewDidEndScrolling(_ scrollView: UIScrollView) {
draggingEnded()
}
}
extension CanvasViewController {
func magnificationStarted() {
guard !renderer.updatesViewPort else { return }
canvas.updateClipBounds(scrollView, on: drawingView)
drawingView.disableUserInteraction()
renderer.updatesViewPort = true
}
func magnificationEnded() {
renderer.updatesViewPort = false
renderer.resize(on: renderView, to: renderView.drawableSize)
renderView.draw()
drawingView.enableUserInteraction()
}
func draggingStarted() {
guard !renderer.updatesViewPort else { return }
canvas.updateClipBounds(scrollView, on: drawingView)
drawingView.disableUserInteraction()
renderer.updatesViewPort = true
}
func draggingEnded() {
renderer.updatesViewPort = false
renderer.resize(on: renderView, to: renderView.drawableSize)
renderView.draw()
drawingView.enableUserInteraction()
}
}
extension CanvasViewController {
func penChanged(to pen: Pen?) {
if let pen, let device = drawingView.renderView.device {
pen.style.loadTexture(on: device)
}
let isPenSelected = pen != nil
scrollView.isScrollEnabled = !isPenSelected
drawingView.isUserInteractionEnabled = isPenSelected
isPenSelected ? drawingView.enableUserInteraction() : drawingView.disableUserInteraction()
}
}
extension CanvasViewController {
func historyUndid() {
guard history.undo() else { return }
drawingView.disableUserInteraction()
canvas.graphicContext.undoGraphic()
renderer.redrawsGraphicRender = true
renderer.resize(on: renderView, to: renderView.drawableSize)
renderView.draw()
drawingView.enableUserInteraction()
}
func historyRedid() {
guard let event = history.redo() else { return }
drawingView.disableUserInteraction()
canvas.graphicContext.redoGraphic(for: event)
renderer.redrawsGraphicRender = true
renderer.resize(on: renderView, to: renderView.drawableSize)
renderView.draw()
drawingView.enableUserInteraction()
}
}

View File

@@ -0,0 +1,68 @@
//
// DrawingView.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import UIKit
import MetalKit
import Foundation
class DrawingView: UIView {
let tool: Tool
let canvas: Canvas
let history: History
let renderView: MTKView
var ratio: CGFloat { canvas.size.width / bounds.width }
private var disablesUserInteraction: Bool = false
required init(tool: Tool, canvas: Canvas, history: History) {
self.tool = tool
self.canvas = canvas
self.history = history
self.renderView = MTKView(frame: .zero)
super.init(frame: .zero)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateDrawableSize(with size: CGSize) {
renderView.drawableSize = size.multiply(by: 2.5)
}
func touchBegan(on point: CGPoint) {
guard !disablesUserInteraction else { return }
guard let pen = tool.selectedPen else { return }
let stroke = canvas.beginTouch(at: point.muliply(by: ratio), pen: pen)
renderView.draw()
history.addUndo(.stroke(stroke))
history.resetRedo()
}
func touchMoved(to point: CGPoint) {
guard !disablesUserInteraction else { return }
canvas.moveTouch(to: point.muliply(by: ratio))
renderView.draw()
}
func touchEnded(to point: CGPoint) {
guard !disablesUserInteraction else { return }
canvas.endTouch(at: point.muliply(by: ratio))
renderView.draw()
}
func disableUserInteraction() {
disablesUserInteraction = true
}
func enableUserInteraction() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
self?.disablesUserInteraction = false
}
}
}

View File

@@ -0,0 +1,20 @@
//
// CanvasView.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import SwiftUI
struct CanvasView: UIViewControllerRepresentable {
@EnvironmentObject var tool: Tool
@EnvironmentObject var canvas: Canvas
@EnvironmentObject var history: History
func makeUIViewController(context: Context) -> CanvasViewController {
CanvasViewController(tool: tool, canvas: canvas, history: history)
}
func updateUIViewController(_ uiViewController: CanvasViewController, context: Context) { }
}

View File

@@ -0,0 +1,17 @@
//
// Array++.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Foundation
extension Array {
mutating func append(_ element: Element, capacity: Int) {
if count >= capacity {
remove(at: 0)
}
append(element)
}
}

View File

@@ -0,0 +1,14 @@
//
// CGAffineTransform++.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Foundation
extension CGAffineTransform {
static func * (lhs: CGAffineTransform, rhs: CGAffineTransform) -> CGAffineTransform {
return lhs.concatenating(rhs)
}
}

View File

@@ -0,0 +1,14 @@
//
// CGFloat++.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Foundation
extension CGFloat {
var float: Float {
Float(self)
}
}

View File

@@ -0,0 +1,29 @@
//
// CGPoint++.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Foundation
extension CGPoint {
func muliply(by factor: CGFloat) -> CGPoint {
CGPoint(x: x * factor, y: y * factor)
}
func distance(to point: CGPoint) -> CGFloat {
let p = pow(x - point.x, 2) + pow(y - point.y, 2)
return sqrt(p)
}
static func middle(p1: CGPoint, p2: CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5)
}
func angle(to point: CGPoint) -> CGFloat {
let deltaX = point.x - x
let deltaY = point.y - y
return atan2(deltaY, deltaX)
}
}

View File

@@ -0,0 +1,19 @@
//
// CGRect++.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Foundation
extension CGRect {
func transform(to rect: CGRect) -> CGAffineTransform {
var t = CGAffineTransform.identity
t = t.translatedBy(x: -self.minX, y: -self.minY)
t = t.translatedBy(x: rect.minX, y: rect.minY)
t = t.scaledBy(x: 1 / self.width, y: 1 / self.height)
t = t.scaledBy(x: rect.width, y: rect.height)
return t
}
}

View File

@@ -0,0 +1,14 @@
//
// CGSize++.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Foundation
extension CGSize {
func multiply(by scale: CGFloat) -> CGSize {
CGSize(width: width * scale, height: height * scale)
}
}

View File

@@ -0,0 +1,21 @@
//
// Collection++.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Foundation
extension Collection {
subscript(safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
extension Collection where Index == Int {
subscript(fromEnd index: Index) -> Element? {
let i = count - (index + 1)
return self[safe: i]
}
}

View File

@@ -0,0 +1,27 @@
//
// Color++.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import SwiftUI
extension Color {
var components: [CGFloat] {
let color = UIColor(self)
return color.components
}
static func rgba(from color: [CGFloat]) -> Color {
Color(red: color[0], green: color[1], blue: color[2]).opacity(color[3])
}
}
extension UIColor {
var components: [CGFloat] {
let uiColor: UIColor = self
let ciColor: CIColor = .init(color: uiColor)
return [ciColor.red, ciColor.green, ciColor.blue, ciColor.alpha]
}
}

View File

@@ -0,0 +1,14 @@
//
// MTLDevice++.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
extension MTLDevice {
func maximumTextureDimension() -> Int {
supportsFamily(.apple3) ? 16384 : 8192
}
}

View File

@@ -0,0 +1,21 @@
//
// simd_float4x4++.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import MetalKit
import Foundation
extension simd_float4x4 {
init(_ transform: CGAffineTransform) {
let t = CATransform3DMakeAffineTransform(transform)
self = simd_float4x4([
[Float(t.m11), Float(t.m12), Float(t.m13), Float(t.m14)],
[Float(t.m21), Float(t.m22), Float(t.m23), Float(t.m24)],
[Float(t.m31), Float(t.m32), Float(t.m33), Float(t.m34)],
[Float(t.m41), Float(t.m42), Float(t.m43), Float(t.m44)]
])
}
}